LongLong's Blog

分享IT技术,分享生活感悟,热爱摄影,热爱航天。

使用OpenAcc进行GPU并行加速

对于多线程的并行计算,常用的基于编译器的框架主要有OpenMP,其通过编译器指令指导编译器对代码进行并行化处理,具有平台无关且对代码改动小的优点。而随着GPU性能的提升,运算密集的应用场景中大量的运算工作已经开始交由GPU来完成,目前主要的编程工具有OpenCL、CUDA、C++ AMP等,可以通过编写特定的GPU代码将一部分运行转交GPU执行。

OpenAcc则采用类似OpenMP的思路,通过编译器指令指导编译器将代码转化为GPU代码,从而实现使用GPU运行特定的代码段。目前支持OpenAcc的编译器主要有PGI,GCC9,而真正能够比较容易的实现转化为GPU代码的编译器目前只有PGI(GCC需要结合CUDA进行编译才具备OpenACC特性)。

1. PGI的安装

PGI的安装较为简单,在PGI下载页面下载最新版本的社区版本即可,其可以支持Windows、Linux和macOS三种操作系统,GPU只支持CUDA(由于PGI已被nVIDIA收购,在新版本中已经移除了对AMD GPU的支持)。

2. OpenACC的基本语法

OpenAcc的基本语法与OpenMP非常相似,对于简单的循环进行并行化的代码如下

int main() {
    const int N = 1000;
    double A[N];
    //完全交由编译器进行并行优化
    #pragma acc kernels
    for (int i = 0; i < N; i++) {
        A[N] = (double) i * i;
    }
    return 0;
}

kernels表示对下面的循环进行并行化处理,处理的方式完全由编译器决定,编译的命令为

pgcc --acc -O2 main.c

编译后的程序实际是生成了部分CUDA代码,运行过程和CUDA编写的代码没有本质区别,过程为首先将数据从内存复制到GPU的专用内存,GPU完成运算后再将数据复制到内存。如此大部分程序都可以简单的通过增加编译指令完成并行化。

3. 性能对比

使用一个矩阵乘法的例子进行未并行化和并行化的运行速度对比,其代码如下

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <omp.h>
int main() {
    const int N = 5000;
    double A[N][N];
    double B[N][N];
    double C[N][N];
    //初始化矩阵
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            A[i][j] = (double) rand() / (double) RAND_MAX;
            B[i][j] = (double) rand() / (double) RAND_MAX;
            C[i][j] = 0.0;
        }
    }
    //矩阵乘法
    double t1 = omp_get_wtime();
    #pragma acc kernels
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            for (int k = 0; k < N; k++) {
                C[i][j] += A[i][k] * B[k][j];
            }
        }
    }
    double t2 = omp_get_wtime();
    printf("%f\n", t2 - t1);
    //输出一个值防止因为未使用而被编译器优化到计算的部分
    printf("%f\n", C[N -1][N - 1]);
}

在开启和未开启OpenAcc的情况下运行此代码,其中矩阵乘法部分的运行时间分别为10s和44s,CPU为Ryzen 3600,GPU为GTX1050Ti,其提升还是非常明显的。

使用Keras实现文本分类

目前已经广泛使用深度学习的方法进行数据分类,包括文本、图像、声音和视频等。其中Tensorflow是使用最为广泛的一种深度学习框架,其能够实现分布式以及使用GPU提高建立模型的速度,但其缺点是代码流程较为复杂,而Keras实现了对Tensorflow API的高层封装,使其相比Tensorflow更加简单易用,目前Keras已经作为Tensorflow中的一部分与Tensorflow一起发布。

1. 数据来源

为了进行模型的训练过程,需要获得一定量的样本数据,本文中的样本数据来自某问答平台,将用户提问标题的文本和问题所属的分类作为学习的训练集。目前Python的pyquery库很好的模拟了jQuery的风格,相比使用正则表达式,其能够更便利的从html文本中提取到所需位置的数据,获取数据的脚本如下

k = 0;
#对于每个分类的地址将获取到的问题写入一个文件
for url in urls:
    k += 1;
    fp = open(str(k) + '.txt', 'w');
    #获取该分类下的问题总数
    request = urllib.request.Request(url = url,  headers = headers);
    try:
        res = urllib.request.urlopen(request, timeout=3);
        html = res.read().decode('gbk');
    except Exception as e:
        print(e);
        continue;
    #计算页数
    num = int(jQuery(html).find('span.count-num')[0].text);
    page = num // size;

    #按页进行数据获取
    for i in range(0, page):
        request = urllib.request.Request(url = url + '&rn=' + str(size) + '&pn=' + str(i * size),  headers = headers);
        #如果出现错误就放弃当页的数据,防止整个程序退出
        try:
            res = urllib.request.urlopen(request, timeout=3);
            html = res.read().decode('gbk');
        except Exception as e:
            print(e);
            continue;
        #通过pyquery获取保存问题标题的a标签
        for x in jQuery(html).find('a.title-link'):
            fp.write(x.text.strip() + "\n");
        time.sleep(2);
    fp.close();

2. 通过数据生成训练模型

生成训练模型的思路为使用经典和简单的TF-IDF方法(这里没有使用目前更加流行的word2vec基于卷积神经网络的方法),对每个问题的标题进行分词,并计算出主要词汇的TF-IDF值生成对应每个问题的向量,问题所属的分类则作为该问题的分类值。

首先需要根据全部的问题生成总的词汇表——每个词汇对应一个唯一的索引值(0~词汇表长度-1),同时也决定了对于每一个问题对应的向量的长度,即train_x数组的维度为“问题个数x词汇表的长度”,train_y数组的维度则为“问题个数x1”,其每一个元素为该个问题的分类索引值。训练过程的代码如下

import jieba.analyse;
import numpy;
import pickle;
import tensorflow.keras;
import scipy.sparse;
from tensorflow.keras.layers import Dense, Activation;
if __name__ == '__main__':
    category = 20;
    k = 0;
    j = 0;
    #生成词汇表
    words = dict();
    for i in range(0, category):
        #对于每个文件(该分类)中的问题
        fp = open(str(i + 1) + '.txt');
        for x in fp:
            j += 1;
            #进行TF-IDF分词,IDF使用分词引擎中的数据
            for y in jieba.analyse.extract_tags(x, withWeight=True):
                #如果是新词,则更新词汇表
                if y[0] not in words:
                    words[y[0]] = k;
                    k += 1;
        fp.close();
    #将词汇表保存,以便于预测时使用
    pickle.dump(words, open('words.bin', 'wb'));
    #确定了两个数组的维度,这里使用稀疏矩阵可以节约一定的内存空间
    train_x = scipy.sparse.lil_matrix((j, len(words)));
    train_y = numpy.zeros((j, 1));
    j = 0;
    #将分类索引值和TF-IDF值填入对应的数组中
    for i in range(0, category):
        fp = open(str(i + 1) + '.txt');
        for x in fp:
            #填入分类索引值
            train_y[j] = i;
            for y in jieba.analyse.extract_tags(x, withWeight=True):
                #填入TF-IDF
                train_x[j, words[y[0]]] = y[1];
            j = j + 1;
    #初始化模型
    model = tensorflow.keras.Sequential();
    #设置网络
    model.add(Dense(32, activation='relu', input_dim=len(words)));
    model.add(Dense(category, activation='sigmoid'));
    #设置分类的优化器
    model.compile(optimizer='rmsprop',
        loss='categorical_crossentropy',
        metrics=['accuracy']);
    #处理分类索引值数组
    train_y = tensorflow.keras.utils.to_categorical(train_y, num_classes=category);
    #开始训练
    model.fit(train_x, train_y, epochs=10, batch_size=32);
    #保存训练得到的模型
    model.save('1.h5');

运行训练过程的输出结果如下,可见10轮训练后的准确率就比较高了

Epoch 1/10
14921/14921 [==============================] - 4s 289us/sample - loss: 2.9050 - accuracy: 0.2525
Epoch 2/10
14921/14921 [==============================] - 4s 269us/sample - loss: 2.3243 - accuracy: 0.6018
Epoch 3/10
14921/14921 [==============================] - 4s 273us/sample - loss: 1.5976 - accuracy: 0.7354
Epoch 4/10
14921/14921 [==============================] - 4s 275us/sample - loss: 1.1616 - accuracy: 0.7962
Epoch 5/10
14921/14921 [==============================] - 4s 274us/sample - loss: 0.8692 - accuracy: 0.8380
Epoch 6/10
14921/14921 [==============================] - 4s 271us/sample - loss: 0.6639 - accuracy: 0.8674
Epoch 7/10
14921/14921 [==============================] - 4s 272us/sample - loss: 0.5143 - accuracy: 0.8899
Epoch 8/10
14921/14921 [==============================] - 4s 267us/sample - loss: 0.4068 - accuracy: 0.9089
Epoch 9/10
14921/14921 [==============================] - 4s 267us/sample - loss: 0.3283 - accuracy: 0.9220
Epoch 10/10
14921/14921 [==============================] - 4s 267us/sample - loss: 0.2700 - accuracy: 0.9335

3. 进行预测

训练得到模型后就可以使用模型对新的问题进行分类预测了,预测的脚本如下

#读取词库
words = pickle.load(open('words.bin', 'rb'));
#加载模型
model = load_model('1.h5');
#需要预测的文本信息,由命令行参数给出
text = sys.argv[1];
#输入向量的维度为1x词汇表长度
data = numpy.zeros((1, len(words)));
#分词并计算TF-IDF
for x in jieba.analyse.extract_tags(text, withWeight=True):
    #如果词汇表中有此词汇则将TF-IDF填入输入向量中
    if x[0] in words:
        data[0, words[x[0]]] = x[1];
#预测,并取出可能性最高的一个分类的索引值
i = numpy.argmax(model.predict(data));
print(labels[i]);

尝试了几个问题,得到的分类结果如下,总体感觉似乎还可以。

健康猫爪子会不会沾染狂犬病毒,请看下面的补充评论谢谢。 -> 生活
平板网络很好,为什么有的软件显示网络不可用?早上还可以! -> 家电数码
普耐尔250二氧化碳焊机手工焊起弧电流什么凋? -> 生产制造
中国的地标城市是在那个地方? -> 旅游