ضرب ماتریس ها در جاوا — به زبان ساده

۷۵۷ بازدید
آخرین به‌روزرسانی: ۰۶ شهریور ۱۴۰۲
زمان مطالعه: ۷ دقیقه
ضرب ماتریس ها در جاوا — به زبان ساده

در این راهنما به بررسی روش ضرب کردن دو ماتریس در هم در زبان برنامه‌نویسی جاوا خواهیم پرداخت. از آنجا که مفهوم ماتریس به صورت بومی در این زبان برنامه‌نویسی وجود ندارد، باید خودمان آن را پیاده‌سازی کنیم. همچنین با چند کتابخانه کار می‌کنیم تا با روش مدیریت دستکاری ماتریس در جاوا آشنا شویم. در نهایت به بررسی عملکرد راه‌حل‌های مختلف نیز می‌پردازیم تا سریع‌ترین رویکرد را بشناسیم.

طراحی مثال

در ابتدا یک مثال آماده می‌کنیم که بتوانیم در سراسر این راهنما به آن ارجاع بدهیم. قبل از هر چیز یک ماتریس 2×3 را تصور کنید.

اکنون ماتریس دومی را نیز تصور کنید که دو ردیف و چهار ستون دارد:

سپس با ضرب ماتریس اول در ماتریس دوم، یک ماتریس 4×3 حاصل می‌شود:

توجه کنید که نتیجه حاصلضرب از طریق ضرب آرایه‌های هر کدام از ماتریس‌ها با فرمول زیر به دست آمده است:

که در آن r تعداد ردیف‌های ماتریس A، c تعداد ستون‌های ماتریس B و n تعداد ستون‌های ماتریس A است که باید با تعداد ردیف‌های ماتریس B مطابقت داشته باشد.

ضرب ماتریس ها در جاوا

در این بخش با روش کدنویسی ضرب ماتریس‌ها آشنا می‌شویم.

پیاده‌سازی

کار خود را با پیاده‌سازی شخصی خودمان از ماتریس‌ها آغاز می‌کنیم. ما این پیاده‌سازی را ساده حفظ می‌کنیم و صرفاً از دو آرایه دابل دوبعدی استفاده می‌کنیم.

1double[][] firstMatrix = {
2  new double[]{1d, 5d},
3  new double[]{2d, 3d},
4  new double[]{1d, 7d}
5};
6 
7double[][] secondMatrix = {
8  new double[]{1d, 2d, 3d, 7d},
9  new double[]{5d, 2d, 8d, 1d}
10};

این‌ها دو ماتریس مثال ما هستند در ادامه ماتریس نتیجه ضرب آن‌ها را نیز ایجاد می‌کنیم:

1double[][] expected = {
2  new double[]{26d, 12d, 43d, 12d},
3  new double[]{17d, 10d, 30d, 17d},
4  new double[]{36d, 16d, 59d, 14d}
5};

اکنون که همه چیز آماده شده است، می‌توانیم الگوریتم ضرب را پیاده‌سازی کنیم. ابتدا یک آرایه نتیجه خالی ایجاد می‌کنیم و حلقه‌ای روی سلول‌های آن تعریف می‌کنیم تا مقدار مورد نظر خود در هر یک از آن‌ها ذخیره کنیم:

1double[][] multiplyMatrices(double[][] firstMatrix, double[][] secondMatrix) {
2    double[][] result = new double[firstMatrix.length][secondMatrix[0].length];
3 
4    for (int row = 0; row < result.length; row++) {
5        for (int col = 0; col < result[row].length; col++) {
6            result[row][col] = multiplyMatricesCell(firstMatrix, secondMatrix, row, col);
7        }
8    }
9 
10    return result;
11}

در نهایت، محاسبه یک سلول منفرد را پیاده‌سازی می‌کنیم. به این منظور از فرمولی که قبلاً ارائه کردیم استفاده می‌کنیم:

1double multiplyMatricesCell(double[][] firstMatrix, double[][] secondMatrix, int row, int col) {
2    double cell = 0;
3    for (int i = 0; i < secondMatrix.length; i++) {
4        cell += firstMatrix[row][i] * secondMatrix[i][col];
5    }
6    return cell;
7}

در نهایت، به بررسی نتیجه تطبیق الگوریتم با نتیجه مورد نظر خود می‌پردازیم:

1double[][] actual = multiplyMatrices(firstMatrix, secondMatrix);
2assertThat(actual).isEqualTo(expected);

EJML

نخستین کتابخانه‌ای که بررسی می‌کنیم EJML است که اختصاری برای عبارت «کتابخانه کارآمد ماتریس جاوا» (Efficient Java Matrix Library) است. در زمان نگارش این مقاله این کتابخانه یکی از به‌روزترین کتابخانه‌های ماتریس جاوا بوده است. هدف این کتابخانه آن است که تا حد امکان از نظر محاسبه و مصرف حافظه بهینه باشد.

ما باید وابستگی به کتابخانه را در فایل pom.xml خود اضافه کنیم:

1<dependency>
2    <groupId>org.ejml</groupId>
3    <artifactId>ejml-all</artifactId>
4    <version>0.38</version>
5</dependency>

چنان که مشاهده می‌کنید ما تقریباً از همان الگوی قبلی استفاده کرده‌ایم، یعنی دو ماتریس بر اساس مثال خود ساختیم و نتیجه دستکاری آن‌ها را با موردی که قبلاً محاسبه شده بود مطابقت دادیم.

بنابراین ماتریس‌های خود را با استفاده از EJML می‌سازیم. به این منظور از کلاس SimpleMatrix استفاده می‌کنیم که این کتابخانه را در اختیار ما قرار می‌دهد.

این کلاس می‌تواند یک آرایه دابل دوبعدی به عنوان ورودی برای سازنده خود بگیرد:

1SimpleMatrix firstMatrix = new SimpleMatrix(
2  new double[][] {
3    new double[] {1d, 5d},
4    new double[] {2d, 3d},
5    new double[] {1d ,7d}
6  }
7);
8 
9SimpleMatrix secondMatrix = new SimpleMatrix(
10  new double[][] {
11    new double[] {1d, 2d, 3d, 7d},
12    new double[] {5d, 2d, 8d, 1d}
13  }
14);

اکنون ماتریس مورد نظر خود را برای دستکاری تعریف می‌کنیم:

1SimpleMatrix expected = new SimpleMatrix(
2  new double[][] {
3    new double[] {26d, 12d, 43d, 12d},
4    new double[] {17d, 10d, 30d, 17d},
5    new double[] {36d, 16d, 59d, 14d}
6  }
7);

اینک که همه چیز راه‌اندازی شده است به بررسی روش دستکاری دو ماتریس با هم می‌پردازیم. کلاس SimpleMatrix یک متد ()mult دارد که کلاس SimpleMatrix دیگری را به عنوان پارامتر می‌گیرد و حاصلضرب دو ماتریس را بازمی‌گرداند:

1SimpleMatrix actual = firstMatrix.mult(secondMatrix);

در ادامه بررسی می‌کنیم که آیا نتیجه به دست آمده با نتیجه مورد انتظار مطابقت دارد یا نه.

از آنجا که SimpleMatrix متد ()equals را override نمی‌کند، نمی‌توانیم برای بررسی، صرفاً به آن تکیه کنیم. اما این کلاس یک متد جایگزین به نام ()isIdentical را نیز ارائه کرده است که نه تنها پارامتر ماتریس دیگر، بلکه یک پارامتر تلرانس خطای double نیز بازگشت می‌دهد که با استفاده از آن می‌توان اختلاف‌های کوچک ناشی از دقت double را نادیده گرفت.

1assertThat(actual).matches(m -> m.isIdentical(expected, 0d));

بدین ترتیب حاصلضرب ماتریس با کتابخانه EJML به دست می‌آید. در ادامه کتابخانه‌های دیگر را مورد بررسی قرار می‌دهیم:

ND4J

اکنون به بررسی کتابخانه ND4J می‌پردازیم. ND4J یک کتابخانه محاسباتی است و بخشی از پروژه deeplearning4j محسوب می‌شود. ND4J علاوه بر قابلیت‌های مختلف، امکان محاسبات ماتریس را نیز ارائه کرده است. قبل از هر چیز باید وابستگی این کتابخانه را به دست آوریم:

1<dependency>
2    <groupId>org.nd4j</groupId>
3    <artifactId>nd4j-native</artifactId>
4    <version>1.0.0-beta4</version>
5</dependency>

توجه داشته باشید که ما از نسخه بتا استفاده می‌کنیم، زیرا به نظر می‌رسد که انتشار GA مقداری باگ دارد.

برای این که همه چیز ساده بماند نما دو آرایه دابل دوبعدی را بازنویسی نمی‌کنیم و صرفاً روی روش استفاده آن‌ها در هر کتابخانه تمرکز می‌کنیم. بدین ترتیب در ND4J باید یک INDArray ایجاد کنیم. به این منظور متد ()Nd4j.create را فراخوانی کرده و آن را به یک آرایه دابل ارسال می‌کنیم که نماینده ماتریس ما است:

1INDArray matrix = Nd4j.create(/* a two dimensions double array */);

همانند بخش قبلی، سه ماتریس می‌سازیم که دو ماتریس قرار است در هم ضرب شوند و یکی دیگر نتیجه مورد انتظار است.

پس از آن باید عمل ضرب بین دو ماتریس اول را عملاً با استفاده از متد ()INDArray.mmul اجرا کنیم:

1INDArray actual = firstMatrix.mmul(secondMatrix);

سپس دوباره بررسی می‌کنیم که نتیجه واقعی با نتیجه مورد انتظار مطابقت دارد یا نه. این بار می‌توانیم روی بررسی برابری تکیه کنیم:

1assertThat(actual).isEqualTo(expected);

این نتیجه نشان می‌دهد که کتابخانه ND4J می‌تواند برای اجرای محاسبات ماتریسی مورد استفاده قرار گیرد.

Apache Commons

در این بخش به بررسی ماژول Math3 کتابخانه Apache Commons می‌پردازیم که محاسبات ریاضیاتی شامل عملیات دستکاری ماتریس را در اختیار ما قرار می‌دهد. این بار نیز باید وابستگی را در pom.xml تعیین کنیم:

1<dependency>
2    <groupId>org.apache.commons</groupId>
3    <artifactId>commons-math3</artifactId>
4    <version>3.6.1</version>
5</dependency>

زمانی که این کتابخانه راه‌اندازی شد، می‌توانیم از اینترفیس RealMatrix و پیاده‌سازی Array2DRowRealMatrix آن برای ایجاد ماتریس‌های معمولی استفاده کنیم. سازنده کلاس پیاده‌سازی، دو آرایه دابل دوبعدی به عنوان پارامترهایش می‌گیرد:

1RealMatrix matrix = new Array2DRowRealMatrix(/* a two dimensions double array */);

اینترفیس RealMatrix برای دستکاری ماتریس یک متد ()multiply ارائه می‌کند که پارامتر RealMatrix دیگری را می‌گیرد:

1RealMatrix actual = firstMatrix.multiply(secondMatrix);

در نهایت می‌توانیم بررسی کنیم که نتیجه مطابق انتظار ما بوده یا نه:

1assertThat(actual).isEqualTo(expected);

در ادامه یک کتابخانه دیگر را بررسی می‌کنیم.

LA4J

کتابخانه‌ای که در این بخش بررسی می‌کنیم، LA4J نام دارد که اختصاری برای عبارت «جبر خطی برای جاوا» (Linear Algebra for Java) است. ابتدا باید وابستگی آن را به پروژه اضافه کنیم:

1<dependency>
2    <groupId>org.la4j</groupId>
3    <artifactId>la4j</artifactId>
4    <version>0.6.0</version>
5</dependency>

اکنون LA4J دقیقاً همانند کتابخانه‌های دیگر کار می‌کند. این کتابخانه یک اینترفیس Matrix با پیاده‌سازی Basic2DMatrix ارائه کرده است که دو آرایه دابل دوبعدی به عنوان ورودی می‌گیرد:

1Matrix matrix = new Basic2DMatrix(/* a two dimensions double array */);

همانند ماژول Math3 در Apache Commons متد ضرب به نام ()multiply است و ماتریس دیگر را به عنوان پارامترش می‌گیرد:

1Matrix actual = firstMatrix.multiply(secondMatrix);

این بار نیز به بررسی نتیجه با ماتریس مورد انتظار می‌پردازیم:

1assertThat(actual).isEqualTo(expected);

اکنون نگاهی به آخرین کتابخانه یعنی Colt می‌اندازیم.

Colt

Colt یک کتابخانه است که از سوی CERN توسعه یافته است. این کتابخانه قابلیت‌هایی ارائه کرده است که امکان اجرای محاسبات فنی و علمی را با عملکرد بالا در اختیار ما قرار می‌دهد.

همانند کتابخانه‌های قبلی باید ابتدا وابستگی صحیح را به پروژه اضافه کنیم:

1<dependency>
2    <groupId>colt</groupId>
3    <artifactId>colt</artifactId>
4    <version>1.2.0</version>
5</dependency>

به منظور ایجاد ماتریس با استفاده از Colt باید از کلاس DoubleFactory2D استفاده کنیم. این کلاس دارای سه وهله factory است که به ترتیب dense ،sparse و rowCompressed نام دارند. هر کدام از این وهله‌ها برای ایجاد نوع خاصی از ماتریس‌ها بهینه‌سازی شده‌اند.

ما در این راهنما از وهله dense استفاده می‌کنیم. این بار متدی که باید فراخوانی شود ()make نام دارد و دو آرایه دابل دوبعدی می‌گیرد و یک شیء DoubleMatrix2D تولید می‌کند:

1DoubleMatrix2D matrix = doubleFactory2D.make(/* a two dimensions double array */);

زمانی که ماتریس‌هایمان وهله‌سازی شدند، می‌توانیم آن‌ها را در هم صرب کنیم. این بار هیچ متدی روی شیء matrix برای انجام این کار وجود ندارد. ما باید یک وهله از کلاس Algebra بسازیم که دارای متد ()mult است. این متد دو ماتریس را به عنوان پارامتر می‌گیرد:

1Algebra algebra = new Algebra();
2DoubleMatrix2D actual = algebra.mult(firstMatrix, secondMatrix);

سپس نتیجه به دست آمده را با ماتریس مورد انتشار بررسی می‌کنیم:

1assertThat(actual).isEqualTo(expected);

بنچمارک

اکنون که کار بررسی روش‌های مختلف دستکاری ماتریس به پایان رسیده است، نوبت آن رسیده که کارآمدترین روش را مشخص کنیم.

برای پیاده‌سازی تست عملکرد از کتابخانه بنچمارکی به نام JMH استفاده می‌کنیم. در ادامه یک کلاس بنچمارک را با گزینه‌های زیر پیکربندی می‌کنیم:

1public static void main(String[] args) throws Exception {
2    Options opt = new OptionsBuilder()
3      .include(MatrixMultiplicationBenchmarking.class.getSimpleName())
4      .mode(Mode.AverageTime)
5      .forks(2)
6      .warmupIterations(5)
7      .measurementIterations(10)
8      .timeUnit(TimeUnit.MICROSECONDS)
9      .build();
10 
11    new Runner(opt).run();
12}

بدین ترتیب JMH دو اجرای کامل برای هر متد حاشیه‌نویسی شده با Benchmark@ تولید می‌کند که هر یک، پنج تکرار گرم کردن اولیه دارند که در محاسبه میانگین دخالت ندارند و 10 اجرای محاسباتی مبنای مقایسه هستند. به منظور محاسبه، زمان میانگین اجرای کتابخانه‌های مختلف برحسب میکروثانیه محاسبه می‌شوند.

سپس باید یک شیء state ایجاد کنیم که شامل آرایه‌های ما است:

1@State(Scope.Benchmark)
2public class MatrixProvider {
3    private double[][] firstMatrix;
4    private double[][] secondMatrix;
5 
6    public MatrixProvider() {
7        firstMatrix =
8          new double[][] {
9            new double[] {1d, 5d},
10            new double[] {2d, 3d},
11            new double[] {1d ,7d}
12          };
13 
14        secondMatrix =
15          new double[][] {
16            new double[] {1d, 2d, 3d, 7d},
17            new double[] {5d, 2d, 8d, 1d}
18          };
19    }
20}

بدین ترتیب مطمئن می‌شویم که مقداردهی اولیه آرایه بخشی از بنچمارک کردن نیست. پس از آن باید متدهایی ایجاد کنیم که دستکاری ماتریس را با استفاده از شیء MatrixProvider به عنوان منبع داده‌ها انجام می‌دهند. ما کد را در این جا تکرار نمی‌کنیم زیرا کتابخانه‌ها را قبلاً مورد بررسی قرار داده‌ایم.

در نهایت فرایند بنچمارک کردن را با استفاده از متد main اجرا می‌کنیم. بدین ترتیب نتیجه زیر حاصل می‌شود:

1Benchmark                                                           Mode  Cnt   Score   Error  Units
2MatrixMultiplicationBenchmarking.apacheCommonsMatrixMultiplication  avgt   20   1,008 ± 0,032  us/op
3MatrixMultiplicationBenchmarking.coltMatrixMultiplication           avgt   20   0,219 ± 0,014  us/op
4MatrixMultiplicationBenchmarking.ejmlMatrixMultiplication           avgt   20   0,226 ± 0,013  us/op
5MatrixMultiplicationBenchmarking.homemadeMatrixMultiplication       avgt   20   0,389 ± 0,045  us/op
6MatrixMultiplicationBenchmarking.la4jMatrixMultiplication           avgt   20   0,427 ± 0,016  us/op
7MatrixMultiplicationBenchmarking.nd4jMatrixMultiplication           avgt   20  12,670 ± 2,582  us/op

چنان که دیدیم EJML و Colt با عملکردی در مدت‌زمان یک‌پنجم میکروثانیه برای هر عملیاتی کارایی بالایی دارند، در حالی که ND4j با زمانی کمی بیشتر از 10 میکروثانیه بر عملیات کارایی کمی دارد. عملکرد کتابخانه‌های دیگر در بین این دو کتابخانه قرار می‌گیرد.

سخن پایانی

در این مقاله، با روش ضرب ماتریس‌ها در جاوا، از طریق کد خودمان و همچنین کتابخانه‌های دیگر آشنا شدیم. پس از بررسی همه راه‌حل‌ها، یک بنچمارک از همه آن‌ها تهیه کردیم و دیدیم که به جز ND4J همه کتابخانه‌های دیگر عملکرد مناسبی دارند. کد کامل این مقاله را می‌توانید در این ریپوی گیت‌هاب (+) ملاحظه کنید.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
baeldung
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *