MapReduce实现矩阵的预处理和矩阵的乘法

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.算法流程

  1. 创建一个Mapper类MatrixMapper,其中map方法用于处理输入数据。首先将输入的一行数据分割成三个部分,分别是行、列和矩阵元素的值。然后将行和列作为输出的键(outKey),矩阵值作为输出的值(outValue),并写入上下文(context)中。接下来,针对该矩阵元素的邻居进行遍历,如果邻居的位置合法并且不与当前元素相同,就将邻居的位置作为输出的键,矩阵值作为输出的值,并写入上下文中。
  2. 创建一个Reducer类MatrixReducer,其中reduce方法用于对Mapper输出的键值对进行处理。首先初始化求和变量sum和计数变量count为0,同时定义一个变量originalValue用于保存当前元素的原始值。然后遍历Mapper输出的值集合,解析每个值的类型和数值。如果值的类型为"M",表示这是一个矩阵元素的原始值,将其保存到originalValue中;如果值的类型为"V",表示这是一个邻居元素的值,将其累加到sum中,并增加计数。最后,计算平均值averagesum/count。如果当前元素的原始值与平均值的绝对差大于2,将原始值更新为平均值。最后,将更新后的值转化为文本形式,并将键值对写入上下文中。
  3. 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.算法流程

  1. 定义了一个静态内部类 MatrixMapper,继承自 Mapper 类。这个类用于将输入的数据切分成键值对,并将它们发送给 Reducer 进行处理。
  2. 在 Mapper 类中定义了一些变量,包括数据集名称(flag)、矩阵A的行数(rowNum)、矩阵B的列数(colNum)、矩阵A当前所在行(rowIndexA)和矩阵B当前所在行(rowIndexB)。
  3. 重写了 Mapper 类的 setup 方法,用于获取输入文件的名称。
  4. 重写了 Mapper 类的 map 方法,在每个 map 方法中首先将输入的文本按空格分隔成一个字符串数组(tokens)。
  5. 根据输入文件的名称(flag),如果是"ma.txt",则进入矩阵A的处理逻辑。对于可能的每一列,创建键(Text对象,表示矩阵A的行和列)和值(Text对象,表示矩阵A的元素)对,并将它们写入上下文(context)。
  6. 如果输入文件的名称是"mb.txt",则进入矩阵B的处理逻辑。对于可能的每一行和每一列,创建键(Text对象,表示矩阵B的行和列)和值(Text对象,表示矩阵B的元素)对,并将它们写入上下文(context)。
  7. 定义了一个静态内部类 MatrixReducer,继承自 Reducer 类。这个类将 Mapper 输出的键值对进行聚合处理,并将结果写入输出。
  8. 重写了 Reducer 类的 reduce 方法。在 reduce 方法中,首先创建了两个空的 HashMap 对象(mapA和mapB),用于存储矩阵A和矩阵B的元素。
  9. 遍历输入的 values,根据键值的不同将元素分别存储到 mapA 或 mapB 中。
  10. 接着,对于每一个矩阵A中的元素,查找对应的矩阵B中的元素,并进行乘法运算,累加结果到 result 变量中。
  11. 最后,将结果写入上下文(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.算法流程

  1. 定义了一个静态内部类 SMMapper 继承自 Mapper 类,并指定了输入键的类型为 LongWritable,输入值的类型为 Text,输出键的类型为 Text,输出值的类型为 Text
  2. 在 setup 方法中获取输入文件的名称,并赋值给变量 flag
  3. 在 map 方法中,根据输入文件的名称判断当前正在处理的是矩阵A的文件还是矩阵B的文件。
  4. 如果是矩阵A的文件(test1.txt),则对于每个输入的行,将该行的元素与矩阵B的列数进行遍历,生成键值对 <行号,列号> -> <'a',行号,元素值>,并通过上下文对象 context 进行输出。
  5. 如果是矩阵B的文件(test2.txt),则对于每个输入的行,将该行的元素与矩阵A的行数进行遍历,生成键值对 <行号,列号> -> <'b',元素值,列号>,并通过上下文对象 context 进行输出。
  6. 定义了一个静态内部类 SMReducer 继承自 Reducer 类,并指定了输入键的类型为 Text,输入值的类型为 Text,输出键的类型为 Text,输出值的类型为 IntWritable
  7. 在 reduce 方法中,创建了两个空的 HashMap,用于存储矩阵A和矩阵B中的元素。
  8. 遍历 values 集合,对每个值进行解析,并根据其类型('a''b')将其放入相应的 HashMap 中。
  9. 初始化变量 result 为0,用于存储最终的乘积结果。
  10. 遍历矩阵A的所有键,如果对应的键在矩阵B中也存在,则将矩阵A和矩阵B中对应键的元素值相乘,并累加到 result 中。
  11. 最后,通过上下文对象 context 将键值对 <行号,列号> -> 乘积结果 输出。

整个算法的流程是:

  1. 输入是两个矩阵A和B,分别存储在两个输入文件 test1.txt 和 test2.txt 中。
  2. SMMapper 类根据输入文件的名称判断当前处理的是矩阵A还是矩阵B,并根据规则生成键值对。
  3. MapReduce 框架将生成的键值对按照键进行分组,并将每组键值对传递给 SMReducer 类的 reduce 方法。
  4. SMReducer 类将相同的键对应的值按照其类型分别放入两个 HashMap 中。
  5. 遍历矩阵A的所有键,如果对应的键在矩阵B中也存在,则将两个矩阵中该键对应的元素值相乘,并累加到结果变量 result 中。
  6. 最后,将乘积结果通过上下文对象 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.输出矩阵

(由于是一位一位排序的,行大概按照1101001112….,列则顺序)

引用借鉴:MapReduce实现矩阵乘法_看山的博客-CSDN博客

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值