1.环境配置
1. VMware虚拟机安装Ubuntu 20.04.6操作系统
2. 在ubuntu中安装jdk
3. 在ubuntu中安装hadoop,并搭建伪分布式环境
4. 在ubuntu中安装eclipse,并安装hadoop插件
2.问题说明
2.1.矩阵的预处理
x1 | x2 | x3 | x4 | x5 | x6 |
x1 | x2 | x3 | x4 | x5 | x6 |
x1 | x2 | x3 | x4 | x5 | x6 |
x1 | x2 | x3 | x4 | x5 | x6 |
x1 | x2 | x3 | x4 | x5 | x6 |
x1 | x2 | x3 | x4 | x5 | x6 |
任意一个值,与它周边一圈的值的均值,差异不能过大。否则,则用均值代替这个值。
2.2.矩阵的乘法
其中矩阵乘法有两个版本算法,包括普通矩阵以及稀疏矩阵的算法
3.理论说明
3.1.矩阵的预处理
3.1.1.思路:
矩阵的预处理形象化来说,就是以一个3乘3的窗口以当前值为中心,将框住的值进行一个平均值的运算,然后进行当前值和均值的插值运算后决定是否进行替换
由于被窗口框到的值需要多次整合起来一起运算,即矩阵中的每一个值均需要多次的调用,因此可以在map阶段就将每个值能够参与哪个位置的值的更新给输出,即每次获取的周围一圈的值的键是自身的位置<I,j>,但是value却是窗口中心的值,最后在reduce阶段即可将相同键值的键值对的value整合起来运算
3.1.2.数据的输入结构
采用每一行的形式为<行,列,值>的形式输入矩阵
如图所示:
3.1.3.具体实现
Map阶段
对来自矩阵的每一行信息进行处理,把<I,j,value>的信息输出为两种键值对,其中一种键值对为<I,j,M,value>来表示窗口中的中心值,另一种键值对为<a,b,V,value>((a,b)表示所有可以取的位置)来表示此中心值能够参与到哪些位置的预处理中
Reduce阶段
对Map阶段发送的键值对进行分类统计,在键值相同的键值对中,类型为M的则将其value赋值给originalValue表示原始值,类型为V的则将其所有的value相加后除以count求出average,最后比较差值是否大于2再决定替补替换原始值,最后输出键值对<key, originalValue/average>
下面是一个简单的3*3矩阵的预处理演示:
3.2.矩阵的乘法
3.2.1.思路
矩阵乘法:
矩阵乘法要求左矩阵的列数与右矩阵的行数相等,m×n的矩阵A,与n×p的矩阵B相乘,结果为m×p的矩阵C。
为了方便描述,先进行假设:
矩阵A的行数为m,列数为n,aij为矩阵A第i行j列的元素。
矩阵B的行数为n,列数为p,bij为矩阵B第i行j列的元素
3.2.2.数据的输入结构
普通矩阵的表示方式:使用最原始的表示方式,相同行内不同列数据通过","分割,不同行通过换行分割,如下形式:
稀疏矩阵的表示方式:通过行列表示法,即文件中的每行数据有三个元素通过分隔符分割,第一个元素表示行,第二个元素表示列,第三个元素表示数据。这种方式对于可以不列出为0的元素,即可以减少稀疏矩阵的数据量,如下形式:
3.2.3.具体实现
map阶段
我们需要对数据进行准备。从矩阵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的哪一行。
在shuffle阶段
Hadoop会自动将具有相同key的value分组到同一个Iterable中,形成<key, Iterable(value)>的对,并将其传递给reduce阶段进行处理。
reduce阶段
通过map阶段的处理,<key, Iterable(value)>中的key(i,j)就对应了矩阵C中的第i行第j列的元素。
Iterable中的每个value在矩阵A和矩阵B的位置也在map阶段进行了标记。对于value(x:y,z),我们只需要找到y相同的来自不同矩阵(即x分别为a和b)的两个元素,将它们相乘后求和即可得到结果。
下面是普通矩阵和稀疏矩阵的简单案例演示:
4.算法实现及分析
4.1.矩阵的预处理
4.1.1.算法流程
- 创建一个Mapper类MatrixMapper,其中map方法用于处理输入数据。首先将输入的一行数据分割成三个部分,分别是行、列和矩阵元素的值。然后将行和列作为输出的键(outKey),矩阵值作为输出的值(outValue),并写入上下文(context)中。接下来,针对该矩阵元素的邻居进行遍历,如果邻居的位置合法并且不与当前元素相同,就将邻居的位置作为输出的键,矩阵值作为输出的值,并写入上下文中。
- 创建一个Reducer类MatrixReducer,其中reduce方法用于对Mapper输出的键值对进行处理。首先初始化求和变量sum和计数变量count为0,同时定义一个变量originalValue用于保存当前元素的原始值。然后遍历Mapper输出的值集合,解析每个值的类型和数值。如果值的类型为"M",表示这是一个矩阵元素的原始值,将其保存到originalValue中;如果值的类型为"V",表示这是一个邻居元素的值,将其累加到sum中,并增加计数。最后,计算平均值average为sum/count。如果当前元素的原始值与平均值的绝对差大于2,将原始值更新为平均值。最后,将更新后的值转化为文本形式,并将键值对写入上下文中。
- 在main方法中,首先创建一个配置对象conf和一个作业对象job。然后设置作业的类和各个阶段的类,包括Mapper类、Reducer类、输出键和输出值的类。接着设置输入和输出路径。最后,调用job.waitForCompletion方法提交作业并等待作业完成,根据作业的执行结果返回相应的退出码。
4.1.2.实现代码
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class MatrixPreprocessing {
// Mapper class
public static class MatrixMapper extends Mapper<LongWritable, Text, Text, Text> {
private Text outKey = new Text();
private Text outValue = new Text();
// Map方法用于处理输入数据
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] parts = value.toString().split(",");
int row = Integer.parseInt(parts[0]);
int col = Integer.parseInt(parts[1]);
double matrixValue = Double.parseDouble(parts[2]);
// 设置输出键和输出值,并写入上下文
outKey.set(row + "," + col);
outValue.set("M," + matrixValue);
context.write(outKey, outValue);
// 遍历邻居元素
for(int i=row-1; i<=row+1; i++){
for(int j=col-1; j<=col+1; j++){
// 判断邻居元素位置的合法性,并且不是当前元素
if(i>0 && i <=3 && j>0 && j <=11 && (i != row || j != col)){
outKey.set(i + "," + j);
outValue.set("V," + matrixValue);
context.write(outKey, outValue);
}
}
}
}
}
// Reducer class
public static class MatrixReducer extends Reducer<Text, Text, Text, Text> {
private Text outValue = new Text();
// Reduce方法用于对Mapper输出的键值对进行处理
public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
double sum = 0.0;
int count = 0;
double originalValue = 0.0;
// 遍历值集合
for (Text value : values) {
String[] parts = value.toString().split(",");
String valueType = parts[0];
double doubleValue = 0.0;
doubleValue = Double.parseDouble(parts[1]);
// 根据值的类型进行处理
if (valueType.equals("M")) {
originalValue = doubleValue;
} else if (valueType.equals("V")) {
sum += doubleValue;
count++;
}
}
// 计算平均值
double average = sum / count;
// 如果当前元素的原始值与平均值的绝对差大于2,则更新原始值为平均值
if (Math.abs(originalValue - average) > 2) {
originalValue = average;
}
// 将更新后的值转化为文本形式,并将键值对写入上下文
outValue.set(Double.toString(originalValue));
context.write(key, outValue);
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "Matrix Preprocessing");
job.setJarByClass(MatrixPreprocessing.class);
job.setMapperClass(MatrixMapper.class);
job.setReducerClass(MatrixReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
// 设置输入和输出路径
FileInputFormat.addInputPath(job, new Path("hdfs://localhost:9000/user/hadoop/input/test.txt"));
FileOutputFormat.setOutputPath(job, new Path("hdfs://localhost:9000/user/hadoop/output"));
// 提交作业并等待作业完成
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
4.2.普通矩阵的乘法
4.2.1.算法流程
- 定义了一个静态内部类 MatrixMapper,继承自 Mapper 类。这个类用于将输入的数据切分成键值对,并将它们发送给 Reducer 进行处理。
- 在 Mapper 类中定义了一些变量,包括数据集名称(flag)、矩阵A的行数(rowNum)、矩阵B的列数(colNum)、矩阵A当前所在行(rowIndexA)和矩阵B当前所在行(rowIndexB)。
- 重写了 Mapper 类的 setup 方法,用于获取输入文件的名称。
- 重写了 Mapper 类的 map 方法,在每个 map 方法中首先将输入的文本按空格分隔成一个字符串数组(tokens)。
- 根据输入文件的名称(flag),如果是"ma.txt",则进入矩阵A的处理逻辑。对于可能的每一列,创建键(Text对象,表示矩阵A的行和列)和值(Text对象,表示矩阵A的元素)对,并将它们写入上下文(context)。
- 如果输入文件的名称是"mb.txt",则进入矩阵B的处理逻辑。对于可能的每一行和每一列,创建键(Text对象,表示矩阵B的行和列)和值(Text对象,表示矩阵B的元素)对,并将它们写入上下文(context)。
- 定义了一个静态内部类 MatrixReducer,继承自 Reducer 类。这个类将 Mapper 输出的键值对进行聚合处理,并将结果写入输出。
- 重写了 Reducer 类的 reduce 方法。在 reduce 方法中,首先创建了两个空的 HashMap 对象(mapA和mapB),用于存储矩阵A和矩阵B的元素。
- 遍历输入的 values,根据键值的不同将元素分别存储到 mapA 或 mapB 中。
- 接着,对于每一个矩阵A中的元素,查找对应的矩阵B中的元素,并进行乘法运算,累加结果到 result 变量中。
- 最后,将结果写入上下文(context),输出键(Text对象,表示矩阵乘法的结果)和值(IntWritable对象,表示计算结果)对。
4.2.2.实现代码
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
public class MatrixMultiply {
public static class MatrixMapper extends
Mapper<LongWritable, Text, Text, Text> {
private String flag = null;// 数据集名称
private int rowNum = 3;// 矩阵A的行数
private int colNum = 3;// 矩阵B的列数
private int rowIndexA = 1; // 矩阵A,当前在第几行
private int rowIndexB = 1; // 矩阵B,当前在第几行
protected void setup(Context context) throws IOException,
InterruptedException {
flag = ((FileSplit) context.getInputSplit()).getPath().getName();// 获取文件名称
}
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String[] tokens = value.toString().split(" ");
if ("ma.txt".equals(flag)) {
for (int i = 1; i <= colNum; i++) {
Text k = new Text(rowIndexA + " " + i);
for (int j = 0; j < tokens.length; j++) {
Text v = new Text("a " + (j + 1) + " " + tokens[j]);
context.write(k, v);
}
}
rowIndexA++;// 每执行一次map方法,矩阵向下移动一行
} else if ("mb.txt".equals(flag)) {
for (int i = 1; i <= rowNum; i++) {
for (int j = 0; j < tokens.length; j++) {
Text k = new Text(i + " " + (j + 1));
Text v = new Text("b " + rowIndexB + " " + tokens[j]);
context.write(k, v);
}
}
rowIndexB++;// 每执行一次map方法,矩阵向下移动一行
}
}
}
public static class MatrixReducer extends
Reducer<Text, Text, Text, IntWritable> {
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
Map<String, String> mapA = new HashMap<String, String>();
Map<String, String> mapB = new HashMap<String, String>();
for (Text value : values) {
String[] val = value.toString().split(" ");
if ("a".equals(val[0])) {
mapA.put(val[1], val[2]);
} else if ("b".equals(val[0])) {
mapB.put(val[1], val[2]);
}
}
int result = 0;
Iterator<String> mKeys = mapA.keySet().iterator();
while (mKeys.hasNext()) {
String mkey = mKeys.next();
if (mapB.get(mkey) == null) {
continue;
}
result += Integer.parseInt(mapA.get(mkey))
* Integer.parseInt(mapB.get(mkey));
}
context.write(key, new IntWritable(result));
}
}
public static void main(String[] args) throws IOException,
ClassNotFoundException, InterruptedException {
String input1 = "hdfs://localhost:9000/user/hadoop/input/ma.txt";
String input2 = "hdfs://localhost:9000/user/hadoop/input/mb.txt";
String output = "hdfs://localhost:9000/user/hadoop/output";
Configuration conf = new Configuration();
conf.addResource("classpath:/hadoop/core-site.xml");
conf.addResource("classpath:/hadoop/hdfs-site.xml");
conf.addResource("classpath:/hadoop/mapred-site.xml");
conf.addResource("classpath:/hadoop/yarn-site.xml");
Job job = Job.getInstance(conf, "MatrixMultiply");
job.setJarByClass(MatrixMultiply.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
job.setMapperClass(MatrixMapper.class);
job.setReducerClass(MatrixReducer.class);
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path(input1), new Path(input2));// 加载2个输入数据集
Path outputPath = new Path(output);
outputPath.getFileSystem(conf).delete(outputPath, true);
FileOutputFormat.setOutputPath(job, outputPath);
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
4.3.稀疏矩阵的乘法
4.3.1.算法流程
- 定义了一个静态内部类 SMMapper 继承自 Mapper 类,并指定了输入键的类型为 LongWritable,输入值的类型为 Text,输出键的类型为 Text,输出值的类型为 Text。
- 在 setup 方法中获取输入文件的名称,并赋值给变量 flag。
- 在 map 方法中,根据输入文件的名称判断当前正在处理的是矩阵A的文件还是矩阵B的文件。
- 如果是矩阵A的文件(test1.txt),则对于每个输入的行,将该行的元素与矩阵B的列数进行遍历,生成键值对 <行号,列号> -> <'a',行号,元素值>,并通过上下文对象 context 进行输出。
- 如果是矩阵B的文件(test2.txt),则对于每个输入的行,将该行的元素与矩阵A的行数进行遍历,生成键值对 <行号,列号> -> <'b',元素值,列号>,并通过上下文对象 context 进行输出。
- 定义了一个静态内部类 SMReducer 继承自 Reducer 类,并指定了输入键的类型为 Text,输入值的类型为 Text,输出键的类型为 Text,输出值的类型为 IntWritable。
- 在 reduce 方法中,创建了两个空的 HashMap,用于存储矩阵A和矩阵B中的元素。
- 遍历 values 集合,对每个值进行解析,并根据其类型('a'或'b')将其放入相应的 HashMap 中。
- 初始化变量 result 为0,用于存储最终的乘积结果。
- 遍历矩阵A的所有键,如果对应的键在矩阵B中也存在,则将矩阵A和矩阵B中对应键的元素值相乘,并累加到 result 中。
- 最后,通过上下文对象 context 将键值对 <行号,列号> -> 乘积结果 输出。
整个算法的流程是:
- 输入是两个矩阵A和B,分别存储在两个输入文件 test1.txt 和 test2.txt 中。
- SMMapper 类根据输入文件的名称判断当前处理的是矩阵A还是矩阵B,并根据规则生成键值对。
- MapReduce 框架将生成的键值对按照键进行分组,并将每组键值对传递给 SMReducer 类的 reduce 方法。
- SMReducer 类将相同的键对应的值按照其类型分别放入两个 HashMap 中。
- 遍历矩阵A的所有键,如果对应的键在矩阵B中也存在,则将两个矩阵中该键对应的元素值相乘,并累加到结果变量 result 中。
- 最后,将乘积结果通过上下文对象 context 输出。
4.3.2.实现代码
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
public class SparseMatrixMultiply {
public static class SMMapper extends Mapper<LongWritable, Text, Text, Text> {
private String flag = null;
private int m = 100;// 矩阵A的行数
private int p = 100;// 矩阵B的列数
protected void setup(Context context) throws IOException,
InterruptedException {
FileSplit split = (FileSplit) context.getInputSplit();
flag = split.getPath().getName();
}
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String[] val = value.toString().split(" ");
if ("test1.txt".equals(flag)) {
for (int i = 1; i <= p; i++) {
context.write(new Text(val[0] + "," + i), new Text("a,"
+ val[1] + "," + val[2]));
}
} else if ("test2.txt".equals(flag)) {
for (int i = 1; i <= m; i++) {
context.write(new Text(i + "," + val[1]), new Text("b,"
+ val[0] + "," + val[2]));
}
}
}
}
public static class SMReducer extends
Reducer<Text, Text, Text, IntWritable> {
protected void reduce(Text key, Iterable<Text> values, Context context)
throws IOException, InterruptedException {
Map<String, String> mapA = new HashMap<String, String>();
Map<String, String> mapB = new HashMap<String, String>();
for (Text value : values) {
String[] val = value.toString().split(",");
if ("a".equals(val[0])) {
mapA.put(val[1], val[2]);
} else if ("b".equals(val[0])) {
mapB.put(val[1], val[2]);
}
}
int result = 0;
// 可能在mapA中存在在mapB中不存在的key,或相反情况
// 因为,数据定义的时候使用的是稀疏矩阵的定义
// 所以,这种只存在于一个map中的key,说明其对应元素为0,不影响结果
Iterator<String> mKeys = mapA.keySet().iterator();
while (mKeys.hasNext()) {
String mkey = mKeys.next();
if (mapB.get(mkey) == null) {
continue;
}
result += Integer.parseInt(mapA.get(mkey))
* Integer.parseInt(mapB.get(mkey));
}
context.write(key, new IntWritable(result));
}
}
public static void main(String[] args) throws IOException,
ClassNotFoundException, InterruptedException {
String input1 = "hdfs://localhost:9000/user/hadoop/input/test1.txt";
String input2 = "hdfs://localhost:9000/user/hadoop/input/test2.txt";
String output = "hdfs://localhost:9000/user/hadoop/output";
Configuration conf = new Configuration();
conf.addResource("classpath:/hadoop/core-site.xml");
conf.addResource("classpath:/hadoop/hdfs-site.xml");
conf.addResource("classpath:/hadoop/mapred-site.xml");
conf.addResource("classpath:/hadoop/yarn-site.xml");
Job job = Job.getInstance(conf, "SparseMatrixMultiply");
job.setJarByClass(SparseMatrixMultiply.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(Text.class);
job.setMapperClass(SMMapper.class);
job.setReducerClass(SMReducer.class);
job.setInputFormatClass(TextInputFormat.class);
job.setOutputFormatClass(TextOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path(input1), new Path(input2));// 加载2个输入数据集
Path outputPath = new Path(output);
outputPath.getFileSystem(conf).delete(outputPath, true);
FileOutputFormat.setOutputPath(job, outputPath);
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
5.运行范例
5.1.矩阵预处理
5.1.1输入矩阵
5.1.2.输出矩阵
5.2.普通矩阵乘法
5.2.1输入矩阵
A矩阵
B矩阵
5.2.2.输出矩阵
5.3.稀疏矩阵乘法
5.2.1输入矩阵
A矩阵
(100*100,值的范围在1-50,稀疏度为0.3)
B矩阵
(100*100,值的范围在1-50,稀疏度为0.4)
5.2.2.输出矩阵
(由于是一位一位排序的,行大概按照1,10,100,11,12….,列则顺序)