使用MapReduce实现矩阵相乘
实现代码
使用Hadoop的MapReduce实现矩阵相乘是可行的,但需要合理设计数据分发和处理逻辑。以下是基于两阶段MapReduce的实现思路:
实现思路
- 阶段一:计算每个k对应的部分乘积。
- 阶段二:聚合所有部分乘积,得到最终结果矩阵。
阶段一:部分乘积计算
Mapper:
- 处理矩阵A的元素,输出键为列号k,值为(
A, 行号i, 值)。 - 处理矩阵B的元素,输出键为行号k,值为(
B, 列号j, 值)。
Reducer:
- 对每个k,收集所有A的行i的值和B的列j的值。
- 计算笛卡尔积,输出键为(i,j),值为A[i][k] * B[k][j]。
示例代码:
/**
* 第一阶段Mapper:负责分发矩阵元素到Reducer
*/
public class Phase1Mapper extends Mapper<Object, Text, Text, Text> {
// 输入值格式示例:
// 矩阵A元素:A,行号i,列号k,值 如 "A,2,3,5.0"
// 矩阵B元素:B,行号k,列号j,值 如 "B,3,4,2.0"
public void map(Object key, Text value, Context context)
throws IOException, InterruptedException {
// 将输入文本按逗号分割
String[] tokens = value.toString().split(",");
String matrix = tokens[0]; // 获取矩阵标识(A或B)
if (matrix.equals("A")) {
// 处理矩阵A的元素
int i = Integer.parseInt(tokens[1]); // 行号i
int k = Integer.parseInt(tokens[2]); // 列号k(即中间维度)
double aVal = Double.parseDouble(tokens[3]); // 元素值
// 输出键为k,值为"A,行号i,值"(通过k进行分组)
// 示例:key="3", value="A,2,5.0"
context.write(
new Text(String.valueOf(k)),
new Text("A," + i + "," + aVal)
);
} else if (matrix.equals("B")) {
// 处理矩阵B的元素
int k = Integer.parseInt(tokens[1]); // 行号k(即中间维度)
int j = Integer.parseInt(tokens[2]); // 列号j
double bVal = Double.parseDouble(tokens[3]); // 元素值
// 输出键为k,值为"B,列号j,值"
// 示例:key="3", value="B,4,2.0"
context.write(
new Text(String.valueOf(k)),
new Text("B," + j + "," + bVal)
);
}
}
}
/**
* 第一阶段Reducer:计算部分乘积
*/
public class Phase1Reducer extends Reducer<Text, Text, Text, DoubleWritable> {
public void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
// 存储来自矩阵A和B的元素
List<String> aEntries = new ArrayList<>(); // 格式:i,value
List<String> bEntries = new ArrayList<>(); // 格式:j,value
// 分类处理来自不同矩阵的值
for (Text val : values) {
String[] parts = val.toString().split(",");
if (parts[0].equals("A")) {
// A矩阵元素:格式为 A,i,value
aEntries.add(parts[1] + "," + parts[2]); // 保存i和值
} else {
// B矩阵元素:格式为 B,j,value
bEntries.add(parts[1] + "," + parts[2]); // 保存j和值
}
}
// 计算笛卡尔积(所有i和j的组合)
for (String a : aEntries) {
String[] aParts = a.split(",");
int i = Integer.parseInt(aParts[0]); // 矩阵A的行号
double aVal = Double.parseDouble(aParts[1]); // 矩阵A的值
for (String b : bEntries) {
String[] bParts = b.split(",");
int j = Integer.parseInt(bParts[0]); // 矩阵B的列号
double bVal = Double.parseDouble(bParts[1]); // 矩阵B的值
// 计算乘积并输出到第二阶段
double product = aVal * bVal;
// 输出键为(i,j),值为乘积结果
// 示例:key="2,4", value=10.0
context.write(
new Text(i + "," + j),
new DoubleWritable(product)
);
}
}
}
}
阶段二:聚合求和
Mapper:直接传递键值对。
Reducer:对相同(i,j)键的值求和。
示例代码:
/**
* 第二阶段Mapper(恒等映射):直接传递键值对
* 输入格式:i,j productValue
*/
public class Phase2Mapper extends Mapper<Object, Text, Text, DoubleWritable> {
public void map(Object key, Text value, Context context)
throws IOException, InterruptedException {
// 将输入行分割为键值对(格式:i,j\tproduct)
String[] parts = value.toString().split("\t");
String outputKey = parts[0]; // 保持键不变
double outputValue = Double.parseDouble(parts[1]);
// 直接传递键值对
context.write(
new Text(outputKey),
new DoubleWritable(outputValue)
);
}
}
/**
* 第二阶段Reducer:求和得到最终结果
*/
public class Phase2Reducer extends Reducer<Text, DoubleWritable, Text, DoubleWritable> {
public void reduce(Text key, Iterable<DoubleWritable> values, Context context)
throws IOException, InterruptedException {
double sum = 0.0;
// 对相同(i,j)的所有部分乘积求和
for (DoubleWritable val : values) {
sum += val.get(); // 累加值
}
// 输出最终结果
context.write(key, new DoubleWritable(sum));
}
}
输入格式要求
// 矩阵A的每个元素需要按以下格式存储:
A,i,k,value
// 例如:A,0,2,3.14 表示A[0][2] = 3.14
// 矩阵B的每个元素需要按以下格式存储:
B,k,j,value
// 例如:B,2,5,6.28 表示B[2][5] = 6.28
// 文件示例:
A,0,0,1.0
A,0,1,2.0
B,0,0,3.0
B,1,0,4.0
运行流程
- 输入格式:矩阵A和B以特定格式存储,如
A,i,k,value和B,k,j,value。 - 执行阶段一:计算每个k对应的部分乘积。
- 执行阶段二:对部分乘积求和,得到最终结果。
注意事项
- 性能问题:阶段一的笛卡尔积可能导致中间数据量爆炸,适用于小规模矩阵或稀疏矩阵。
- 优化:可通过矩阵分块、Combiner优化或调整数据分发策略来减少数据传输。
关键概念图解
矩阵相乘原理:
C[i][j] = Σ(A[i][k] * B[k][j]) ,对k从1到n求和
MapReduce数据流:
矩阵A元素 (i,k) → 通过k分发 → Reducer
矩阵B元素 (k,j) → 通过k分发 → Reducer
Reducer内计算所有i和j的组合乘积
阶段一输出:
(0,0) → 1 * 3 + 2 * 4 = 11
(0,1) → 1 * 5 + 2 * 6 = 17
...(其他元素同理)
阶段二输出:
(0,0) 11
(0,1) 17
...(最终结果矩阵)
通过上述方法,可以利用Hadoop的MapReduce实现矩阵相乘,虽然存在性能限制,但理论上是可行的。
具体例子详细跟踪程序的执行过程
让我们通过一个具体的2x2矩阵相乘示例,详细跟踪程序的执行过程。假设:
矩阵A:
[1 2]
[3 4]
矩阵B:
[5 6]
[7 8]
最终结果C = A×B 应该是:
[1×5+2×7=19, 1×6+2×8=22]
[3×5+4×7=43, 3×6+4×8=50]
输入文件格式
文件内容(假设存储为input/matrix.txt):
A,0,0,1.0
A,0,1,2.0
A,1,0,3.0
A,1,1,4.0
B,0,0,5.0
B,0,1,6.0
B,1,0,7.0
B,1,1,8.0
阶段一执行过程
Mapper处理流程
每个Mapper会处理一行输入:
| 输入行 | 输出键 (k) | 输出值 (v) | 说明 |
|---|---|---|---|
A,0,0,1.0 |
"0" | "A,0,1.0" | 矩阵A第0行第0列元素 |
A,0,1,2.0 |
"1" | "A,0,2.0" | 矩阵A第0行第1列元素 |
A,1,0,3.0 |
"0" | "A,1,3.0" | 矩阵A第1行第0列元素 |
A,1,1,4.0 |
"1" | "A,1,4.0" | 矩阵A第1行第1列元素 |
B,0,0,5.0 |
"0" | "B,0,5.0" | 矩阵B第0行第0列元素 |
B,0,1,6.0 |
"0" | "B,1,6.0" | 矩阵B第0行第1列元素 |
B,1,0,7.0 |
"1" | "B,0,7.0" | 矩阵B第1行第0列元素 |
B,1,1,8.0 |
"1" | "B,1,8.0" | 矩阵B第1行第1列元素 |
Reducer处理过程
按key分组后:
Reducer处理key="0"(k=0)时:
- 收到的values列表:
["A,0,1.0", "A,1,3.0", "B,0,5.0", "B,1,6.0"] - 分类存储:
aEntries = ["0,1.0", "1,3.0"] // 矩阵A第0列元素(即k=0) bEntries = ["0,5.0", "1,6.0"] // 矩阵B第0行元素(即k=0) - 计算笛卡尔积:
(0,0) → 1.0*5.0 = 5.0 (0,1) → 1.0*6.0 = 6.0 (1,0) → 3.0*5.0 = 15.0 (1,1) → 3.0*6.0 = 18.0
Reducer处理key="1"(k=1)时:
- 收到的values列表:
["A,0,2.0", "A,1,4.0", "B,0,7.0", "B,1,8.0"] - 分类存储:
aEntries = ["0,2.0", "1,4.0"] // 矩阵A第1列元素(k=1) bEntries = ["0,7.0", "1,8.0"] // 矩阵B第1行元素(k=1) - 计算笛卡尔积:
(0,0) → 2.0*7.0 = 14.0 (0,1) → 2.0*8.0 = 16.0 (1,0) → 4.0*7.0 = 28.0 (1,1) → 4.0*8.0 = 32.0
阶段一最终输出
0,0 → 5.0
0,1 → 6.0
1,0 → 15.0
1,1 → 18.0
0,0 → 14.0
0,1 → 16.0
1,0 → 28.0
1,1 → 32.0
阶段二执行过程
Mapper处理(直接传递)
输入来自阶段一的输出,Mapper不做处理,直接传递:
(0,0) → 5.0
(0,1) → 6.0
(1,0) → 15.0
...(其他值同理)
Reducer聚合求和
按(i,j)分组求和:
处理(0,0):
- 值列表:[5.0, 14.0]
- 求和:5.0 + 14.0 = 19.0
处理(0,1):
- 值列表:[6.0, 16.0]
- 求和:6.0 + 16.0 = 22.0
处理(1,0):
- 值列表:[15.0, 28.0]
- 求和:15.0 + 28.0 = 43.0
处理(1,1):
- 值列表:[18.0, 32.0]
- 求和:18.0 + 32.0 = 50.0
最终输出结果
0,0 → 19.0
0,1 → 22.0
1,0 → 43.0
1,1 → 50.0
可视化数据流向
矩阵A元素分布:
k=0 → A(0,0)=1.0, A(1,0)=3.0
k=1 → A(0,1)=2.0, A(1,1)=4.0
矩阵B元素分布:
k=0 → B(0,0)=5.0, B(0,1)=6.0
k=1 → B(1,0)=7.0, B(1,1)=8.0
阶段一Reducer处理:
k=0时:
(i=0) * (j=0) → 1*5=5
(i=0) * (j=1) → 1*6=6
(i=1) * (j=0) → 3*5=15
(i=1) * (j=1) → 3*6=18
k=1时:
(i=0) * (j=0) → 2*7=14
(i=0) * (j=1) → 2*8=16
(i=1) * (j=0) → 4*7=28
(i=1) * (j=1) → 4*8=32
阶段二聚合:
C[0][0] = 5 + 14 = 19
C[0][1] = 6 + 16 = 22
C[1][0] = 15 + 28 = 43
C[1][1] = 18 + 32 = 50
通过这个具体案例,可以清晰看到:
- 数据如何通过
k进行分组 - Reducer如何计算部分乘积
- 两阶段MapReduce如何协作完成矩阵相乘
更常见的实现方式
从矩阵A中的元素aij开始,我们将其转化为p个<key, value>对的形式,其中key=“i,k”(其中k=1,2,…,p),value=“a:j,aij”。类似地,从矩阵B中的元素bij开始,我们将其转化为m个<key, value>对的形式,其中key=“k,j”(其中k=1,2,…,m),value=“b:i,bij”。
通过这样的处理,我们就能够得到具有相同key(“i,j”)的数据对,同时通过value中的"a:"和"b:"来区分元素是来自矩阵A还是矩阵B,以及它们在矩阵A的哪一列或矩阵B的哪一行。
我们可以通过以下分步解释来实现这种方法的矩阵相乘,并通过具体示例验证其可行性:
方法原理
该方法的本质是将矩阵元素按所有可能参与计算的维度预先分发,使得Reducer能直接获取到计算C[i][j] = Σ(A[i][k] * B[k][j])所需的全部数据。以下是具体实现步骤:
输入数据格式
矩阵A的行数为m,列数为n,aij为矩阵A第i行j列的元素;矩阵B的行数为n,列数为p,bij为矩阵B第i行j列的元素。
- 矩阵A元素格式:
(i,j,A[i][j]) - 矩阵B元素格式:
(k,j,B[k][j])(注意此处应为B的行号为k)
处理流程
-
Mapper阶段:
- 矩阵A的每个元素
A[i][j]生成p个键值对(假设B的列数为p):- Key:
i,k(k从1到p) - Value:
a:j,A[i][j]
- Key:
- 矩阵B的每个元素
B[k][j]生成m个键值对(假设A的行数为m):- Key:
k,j - Value:
b:i,B[k][j]
- Key:
- 矩阵A的每个元素
-
Shuffle/Sort阶段:
- 系统自动将相同
i,k和k,j组合的键聚合到同一Reducer
- 系统自动将相同
-
Reducer阶段:
- 对每个键
i,k,收集所有a:j和b:i的值 - 计算对应元素的乘积,并输出中间结果
- 对每个键
-
二次聚合阶段(可能需要第二趟MapReduce):
- 按最终结果坐标
i,j求和
- 按最终结果坐标
具体实现步骤
阶段1:元素分发与初步计算
Mapper逻辑:
// 处理矩阵A的元素
public void mapA(int i, int j, double value) {
for (int k = 1; k <= p; k++) { // p为矩阵B的列数
emit("i=" + i + ",k=" + k, "a:j=" + j + ",v=" + value);
}
}
// 处理矩阵B的元素
public void mapB(int k, int j, double value) {
for (int i = 1; i <= m; i++) { // m为矩阵A的行数
emit("k=" + k + ",j=" + j, "b:i=" + i + ",v=" + value);
}
}
Reducer逻辑:
public void reduce(String key, Iterable<String> values) {
Map<Integer, Double> aMap = new HashMap<>(); // 存储j->A值
Map<Integer, Double> bMap = new HashMap<>(); // 存储i->B值
// 分离A、B元素
for (String val : values) {
if (val.startsWith("a:")) {
String[] parts = val.split("j=")[1].split(",v=");
int j = Integer.parseInt(parts[0]);
double aVal = Double.parseDouble(parts[1]);
aMap.put(j, aVal);
} else {
String[] parts = val.split("i=")[1].split(",v=");
int i = Integer.parseInt(parts[0]);
double bVal = Double.parseDouble(parts[1]);
bMap.put(i, bVal);
}
}
// 计算笛卡尔积
for (Entry<Integer, Double> aEntry : aMap.entrySet()) {
int j = aEntry.getKey();
double aVal = aEntry.getValue();
for (Entry<Integer, Double> bEntry : bMap.entrySet()) {
int i = bEntry.getKey();
double bVal = bEntry.getValue();
emit("i=" + i + ",j=" + j, aVal * bVal);
}
}
}
阶段2:结果聚合
二次Mapper(恒等映射):
public void map(String key, double value) {
emit(key, value);
}
二次Reducer:
public void reduce(String key, Iterable<Double> values) {
double sum = 0;
for (Double v : values) {
sum += v;
}
emit(key, sum);
}
具体示例验证
假设:
矩阵A(2x2):
A = [[1, 2],
[3, 4]]
矩阵B(2x2):
B = [[5, 6],
[7, 8]]
阶段1处理流程
矩阵A元素分发:
| 原始元素 | 生成键值对 |
|---|---|
| A[0][0] | ("i=0,k=0", "a:j=0,v=1") ("i=0,k=1", "a:j=0,v=1") |
| A[0][1] | ("i=0,k=0", "a:j=1,v=2") ("i=0,k=1", "a:j=1,v=2") |
| A[1][0] | ("i=1,k=0", "a:j=0,v=3") ("i=1,k=1", "a:j=0,v=3") |
| A[1][1] | ("i=1,k=0", "a:j=1,v=4") ("i=1,k=1", "a:j=1,v=4") |
矩阵B元素分发:
| 原始元素 | 生成键值对 |
|---|---|
| B[0][0] | ("k=0,j=0", "b:i=0,v=5") ("k=0,j=0", "b:i=1,v=5") |
| B[0][1] | ("k=0,j=1", "b:i=0,v=6") ("k=0,j=1", "b:i=1,v=6") |
| B[1][0] | ("k=1,j=0", "b:i=0,v=7") ("k=1,j=0", "b:i=1,v=7") |
| B[1][1] | ("k=1,j=1", "b:i=0,v=8") ("k=1,j=1", "b:i=1,v=8") |
Reducer处理示例(以键"i=0,k=0"为例):
- 接收到的values:
"a:j=0,v=1" (来自A[0][0]) "a:j=1,v=2" (来自A[0][1]) "b:i=0,v=5" (来自B[0][0]) "b:i=1,v=5" (来自B[0][0]) - 计算过程:
aMap = {0:1.0, 1:2.0} bMap = {0:5.0, 1:5.0} 生成乘积: (j=0, i=0) → 1*5 = 5 → 输出("i=0,j=0",5) (j=0, i=1) → 1*5 = 5 → 输出("i=1,j=0",5) (j=1, i=0) → 2*5 = 10 → 输出("i=0,j=1",10) (j=1, i=1) → 2*5 = 10 → 输出("i=1,j=1",10)
阶段1输出结果
// 来自各个Reducer的输出:
("i=0,j=0",5), ("i=0,j=0",14) → 需要求和
("i=0,j=1",6), ("i=0,j=1",16)
("i=1,j=0",15), ("i=1,j=0",28)
("i=1,j=1",18), ("i=1,j=1",32)
阶段2最终结果
i=0,j=0 → 5+14=19
i=0,j=1 → 6+16=22
i=1,j=0 → 15+28=43
i=1,j=1 → 18+32=50
方法分析
优势:
- 并行度更高:允许不同k值的计算完全并行
- 灵活性:适合动态调整矩阵维度
- 容错性:细粒度任务划分
缺陷:
- 数据爆炸:矩阵A每个元素生成p个副本,B每个元素生成m个副本
- 对于n×n矩阵,数据量膨胀到O(n³)
- 存储压力:中间结果需要大量磁盘空间
- 网络开销:Shuffle阶段传输数据量巨大
性能对比
| 指标 | 传统方法 | 本方法 |
|---|---|---|
| Mapper输出 | O(n²) | O(n³) |
| Shuffle数据量 | O(n²) | O(n³) |
| Reducer计算 | O(n³/p) | O(n³/p) |
| 适用场景 | 中小型矩阵 | 理论验证/特殊场景 |
优化建议
-
稀疏矩阵优化:
// 在Mapper中跳过零值 if (value == 0) return; -
Combiner应用:
// 在阶段1后添加局部聚合 job.setCombinerClass(Phase2Reducer.class); -
分块处理:
将矩阵划分为b×b块: - 每个块生成b个副本而非n个 - 数据量降为O(b·n²) -
压缩技术:
<!-- 在mapred-site.xml中启用压缩 --> <property> <name>mapreduce.map.output.compress</name> <value>true</value> </property>
总结
该方法通过空间换时间的策略实现更高并行度,但需要谨慎处理数据膨胀问题。适用于以下场景:
- 理论研究需要极端并行化
- 特殊硬件环境(如超大内存集群)
- 稀疏矩阵计算(需配合零值优化)
实际生产环境中更推荐传统两阶段MapReduce方法,但在学术研究或特定优化场景下,这种多维分发策略可以提供新的思路。理解这种方法有助于深入掌握MapReduce的分发机制和矩阵计算的核心原理。
更复杂的例子
让我们通过一个具体的4×3矩阵A与3×2矩阵B相乘的例子,详细展示这种方法的完整执行过程。假设:
矩阵A(4行3列):
A = [
[1, 2, 3], // 行0
[4, 5, 6], // 行1
[7, 8, 9], // 行2
[10, 11, 12] // 行3
]
矩阵B(3行2列):
B = [
[5, 6], // 行0
[7, 8], // 行1
[9, 10] // 行2
]
预期计算结果
结果矩阵C = A×B 应该是:
C[0][0] = 1*5 + 2*7 + 3*9 = 5+14+27=46
C[0][1] = 1*6 + 2*8 + 3*10=6+16+30=52
C[1][0] = 4*5 +5*7 +6*9=20+35+54=109
C[1][1] = 4*6+5*8+6*10=24+40+60=124
...其他行同理
数据预处理
输入文件格式(假设存储为input/matrix.txt):
A,0,0,1.0
A,0,1,2.0
A,0,2,3.0
A,1,0,4.0
A,1,1,5.0
A,1,2,6.0
A,2,0,7.0
A,2,1,8.0
A,2,2,9.0
A,3,0,10.0
A,3,1,11.0
A,3,2,12.0
B,0,0,5.0
B,0,1,6.0
B,1,0,7.0
B,1,1,8.0
B,2,0,9.0
B,2,1,10.0
以下是用表格形式详细展示4x3矩阵A(行号i,列号j)与3x2矩阵B(行号k,列号j)相乘的完整处理过程:
矩阵定义
| 矩阵 | 维度 | 示例元素 |
|---|---|---|
| A | 4x3 | A[0][0]=1.0 |
| B | 3x2 | B[0][0]=5.0 |
阶段1:Mapper处理
矩阵A元素处理(以A[0][0]=1.0为例)
| 元素坐标 | 生成键值对 | 说明 |
|---|---|---|
| (0,0) | key="i=0,k=0" → value="a:j=0,1.0" | 向所有k维度分发(B的列数=2) |
| key="i=0,k=1" → value="a:j=0,1.0" |
矩阵B元素处理(以B[0][0]=5.0为例)
| 元素坐标 | 生成键值对 | 说明 |
|---|---|---|
| (0,0) | key="k=0,j=0" → value="b:i=0,5.0" | 向所有i维度分发(A的行数=4) |
| key="k=0,j=0" → value="b:i=1,5.0" | ||
| key="k=0,j=0" → value="b:i=2,5.0" | ||
| key="k=0,j=0" → value="b:i=3,5.0" |
阶段2:Shuffle过程
键分组示例(以key="i=0,k=0"和key="k=0,j=0"为例)
| 键 | 收到的值集合 |
|---|---|
| "i=0,k=0" | ["a:j=0,1.0", "a:j=1,2.0", "a:j=2,3.0"] (来自A的第0行所有元素) |
| "k=0,j=0" | ["b:i=0,5.0", "b:i=1,5.0", "b:i=2,5.0", "b:i=3,5.0"] (来自B的第0行第0列) |
阶段3:Reducer处理
处理key="i=0,k=0"
| 处理步骤 | 数据示例 | 说明 |
|---|---|---|
| 1. 分离A/B元素 | A元素:j=0→1.0, j=1→2.0, j=2→3.0 B元素:i=0→5.0, i=1→5.0, i=2→5.0, i=3→5.0 |
建立j→A值和i→B值的映射 |
| 2. 计算笛卡尔积 | j=0 * i=0 → 1.05.0=5.0 j=1 * i=0 → 2.05.0=10.0 ... |
生成部分乘积 |
| 3. 输出中间结果 | ("i=0,j=0",5.0) ("i=0,j=0",10.0) ("i=0,j=0",15.0) |
需要后续求和 |
完整流程示例(计算C[0][0])
| 步骤 | Mapper输出示例 | Shuffle后数据 | Reducer处理 | 输出结果 |
|---|---|---|---|---|
| 1 | A[0][0]=1 → ("i=0,k=0","a:0,1") A[0][1]=2 → ("i=0,k=0","a:1,2") A[0][2]=3 → ("i=0,k=0","a:2,3") |
key="i=0,k=0" → [a:0,1; a:1,2; a:2,3] | 解析A元素的j值:0→1,1→2,2→3 | |
| 2 | B[0][0]=5 → ("k=0,j=0","b:0,5") B[1][0]=7 → ("k=0,j=0","b:0,7") B[2][0]=9 → ("k=0,j=0","b:0,9") |
key="k=0,j=0" → [b:0,5; b:0,7; b:0,9] | 解析B元素的k值:0→5,1→7,2→9 | |
| 3 | - | 系统自动合并相关键 | 计算乘积: 15=5 27=14 3*9=27 |
("0,0",5) ("0,0",14) ("0,0",27) |
| 4 | - | 二次Shuffle聚合 | 求和:5+14+27=46 | ("0,0",46) |
全流程数据流总表
| 阶段 | 输入数据 | 处理逻辑 | 输出数据格式 |
|---|---|---|---|
| Mapper | A[i][j]或B[k][j] | A:生成p个(i,k)键(p=B的列数) B:生成m个(k,j)键(m=A的行数) |
key="i,k"→"a:j,v" key="k,j"→"b:i,v" |
| Shuffle | 键值对 | 按key分组,相同key的数据发送到同一Reducer | key→[values]列表 |
| Reducer | 同一key的A/B元素集合 | 1. 分离A的(j→v)和B的(i→v) 2. 计算所有j-i组合的乘积 |
key="i,j"→乘积结果 |
| 最终聚合 | 所有(i,j)的乘积结果 | 按(i,j)分组求和 | key="i,j"→最终矩阵元素值 |
性能特征表
| 矩阵维度 | 阶段 | 数据量 | 计算复杂度 |
|---|---|---|---|
| A:4x3 | Mapper输出 | 4x3x2=24条 | O(n³) |
| B:3x2 | Mapper输出 | 3x2x4=24条 | O(n³) |
| Shuffle | 中间数据 | 24+24=48条 | - |
| Reducer | 乘积计算次数 | 4x2x3=24次 | O(n³) |
异常处理案例表
| 异常场景 | 检测方法 | 处理方案 |
|---|---|---|
| 矩阵维度不匹配 | 检查A的列数是否等于B的行数 | 抛出IllegalArgumentException |
| 非数值型数据 | try-catch解析数值 | 记录错误计数器,跳过该记录 |
| 数据格式错误 | 验证token数量是否=4 | 写入错误日志,忽略错误行 |
| 内存溢出 | 监控values集合大小 | 分批次处理或报错终止 |
通过这个完整的表格化流程展示,可以清晰看到:
- 数据分发策略:通过设计键结构确保必要元素的相遇
- 计算并行性:不同(i,k)对的Reducer可以完全并行执行
- 数据膨胀问题:Mapper输出量是原始数据的O(n³)倍
- 最终聚合需求:必须进行二次求和才能得到正确结果
这种可视化展示方式有助于理解MapReduce处理矩阵相乘的核心机制,同时也突显了该方法在大规模数据场景下的潜在性能瓶颈。

浙公网安备 33010602011771号