CUDA编程之矩阵乘法

news2024/11/15 4:00:44

文章目录

  • 一、矩阵乘法回顾
  • 二、CUDA内存架构
    • CUDA中的共享内存
    • CUDA中的共享内存使用方法
    • 静态申请内存
    • 动态申请内存
  • 三、分解矩阵乘法 / 平铺矩阵乘法
  • 四、实战代码
    • DS_M 和 DS_N的索引方式解释


一、矩阵乘法回顾

CPU版本:
请添加图片描述
GPU版本: 请添加图片描述
核函数如下:
请添加图片描述
C = AB ([mk],[kn])的矩阵乘法运算,每个线程都要读取A的一整行和B的一整列。A矩
阵中的每个点需要被读N次,B矩阵中的每个点需要被读M次。
因此我们可以将多次访问的数据放到共享内存中,减少重复读取的次数,并充分利用共享内存
的延迟低的优势。

二、CUDA内存架构

对于不同的寄存器和硬件,线程读写速度对比如下:

  1. 各自线程寄存器(~1周期)
  2. 线程块共享内存(~5周期)
  3. Grid全局内存(~500周期)
  4. Grid常量内存(~5周期)
    因此,我们可以把频繁IO的变量放在线程块共享内存。

CUDA中的共享内存

CUDA中的共享内存是一种特殊类型的内存,其内容在源代码中被显式声明和使用
• 位于处理器中
• 以更高的速度访问(延迟&吞吐) • 仍然被内存访问指令访问
• 在计算机体系结构中通常称为暂存存储器

共享内存有以下特点:
• 读取速度等同于缓存,在很多显卡上,缓存和共享内存使用的是同一块硬
件,并且可以配置大小
• 共享内存属于线程块,可以被一个线程块内的所有线程访问
• 共享内存的两种申请空间方式,静态申请和动态申请
• 共享内存的大小只有几十K,过度使用共享内存会降低程序的并行性

CUDA中的共享内存使用方法

1.申请
• __shared__关键字 • 静态申请
• 动态申请

2.使用
• 将每个线程从全局索引位置读取元素,将它存储到共享内存之中。
• 注意数据存在着交叉,应该将边界上的数据拷贝进来。
• 块内线程同步: __syncthreads()。

注意,在使用__syncthreads()同步线程的时候,请避免使用如下同步的方法:

__share__ val[];
if(index < n){
  if(tid condition){
      do something with val; __syncthreads();
 } else{
      do something with val; __syncthreads();
}
}

该种写法会造成线程发散,举个例子,假设我们有一个简单的线程块,包含4个线程,要求偶数线程和奇数线程分别执行不同的操作:

__global__ 
void exampleKernel(float* data) {
    int tid = threadIdx.x;
    int index = threadIdx.x + blockDim.x * blockIdx.x;
    __shared__ float val[4];

    if (index < 4) {
        if (tid % 2 == 0) {
            val[tid] = data[tid] * 2;
        } else {
            val[tid] = data[tid] * 3;
        }
        __syncthreads();
        // Do some other operations here after synchronization
    }
}

静态申请内存

__global__ void staticReverse(int *d,int n){
   __shared__ int s[64]; //内存大小固定
   int t = threadIdx.x;
   int tr = n - t - 1;
   s[t] = d[t]; 
   __syncthreads();
   d[t] = s[tr]; //这一步没有对共享内存进行操作,所以不需要sync
}
staticReverse<<<1,n>>>(d_d,n);

动态申请内存

__global__ void dynamicReverse(int *d,int n){
	extern __shared__ int s[];
	int t= threadIdx.x;
	int tr = n - 1- t;
	s[t] = d[t];
	__syncthreads();
	d[t] = s[tr];
}
dynamicReverse<<<1,n,n * sizeof(int)>>> (d_d,n); //核函数被调用的时候需要传入动态内存的大小

三、分解矩阵乘法 / 平铺矩阵乘法

为了高效利用共享内存,我们使用一种叫做分解矩阵乘法的算法:
请添加图片描述
请添加图片描述
请添加图片描述
对于平铺矩阵算法,对于每一个Block,首先将其存储在共享内存中, 花费 * Block_Size ^ 2,对BLock_SIze中每一个元素进行读取需要花费: 2 * k / Block_Size 个单位时间,一共有Block_Size ^ 2个元素, 一共有 (m * n) / (block_size * block_size)个Block,所以总时间就是这些值进行相乘。
因此理论加速比为Block_size,考虑到同步函数的开销以及共享内存读写的开销,实际加速比会比这个低。

四、实战代码

#include<bits/stdc++.h>
#include<sys/time.h>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include "cublas_v2.h"
#define M 512 
#define K 512
#define N 512
#define BLOCK_SIZE 32 
using namespace std;
void initial(float *array, int size){
    for(int i=0;i<size;i++){
        array[i] = (float)(rand() % 100 + 1);
    }
}
void printMatrix(float *array,int row,int col){
    float *p = array;
    for(int y=0;y<row;y++){
      for(int x=0;x<col;x++){
        printf("%10lf",p[x]);
      }
      p = p + col;
      printf("\n");
    }
}
void multiplicateMatrixonHost(float *array_A,float *array_B,float *array_C,int M_p,int K_p,int N_p){
   for(int i =0 ;i < M_p;i++){
    for(int j = 0;j < N_p;j++){
        float sum = 0;
        for(int k = 0;k < K_p;k++){
            sum += array_A[i * K_p + k] * array_B[k * N_p + j];
        }
        array_C[i * N_p + j] = sum;
    }
   }
}
__global__ void multiplicateMatrixonDevice(float *array_A,float *array_B,float *array_C,int M_p,int K_p,int N_p){
    int iy = blockIdx.y * blockDim.y + threadIdx.y;
    int ix = blockIdx.x * blockDim.x + threadIdx.x;
    if(iy < M_p && ix < N_p){
        float sum = 0;
        for(int i = 0;i < K_p;i++){
            sum += array_A[iy * K_p + i] * array_B[i * N_p + ix]; 
        }
        array_C[iy * N_p + ix] = sum;
    }
}
//Compute C = A * B 
__global__ void matrixMultiplyShared(float *A, float *B, float *C, int numARows,
                                     int numAColumns, int numBRows,
                                     int numBColumns, int numCRows,
                                     int numCColumns) {
  __shared__ float ds_M[BLOCK_SIZE][BLOCK_SIZE];
  __shared__ float ds_N[BLOCK_SIZE][BLOCK_SIZE];
  int bx = blockIdx.x;
  int by = blockIdx.y;
  int tx = threadIdx.x;
  int ty = threadIdx.y;
  int Row = by * BLOCK_SIZE + ty;
  int Col = bx * BLOCK_SIZE + tx;
  float Pvalue = 0;
  for (int m = 0; m < (numAColumns - 1) / BLOCK_SIZE + 1; ++m) {
    if (Row < numARows && m * BLOCK_SIZE + tx < numAColumns) {
      ds_M[ty][tx] = A[Row * numAColumns + m * BLOCK_SIZE + tx];
    } else {
      ds_M[ty][tx] = 0.0;
    }
    if (Col < numBColumns && m * BLOCK_SIZE + ty < numBRows) {
      ds_N[ty][tx] = B[(m * BLOCK_SIZE + ty) * numBColumns + Col];
    } else {
      ds_N[ty][tx] = 0.0;
    }
    __syncthreads();
    for (int k = 0; k < BLOCK_SIZE; ++k) {
      Pvalue += ds_M[ty][k] * ds_N[k][tx];
    }
    __syncthreads();
  }
  if (Row < numCRows && Col < numCColumns) {
    C[Row * numCColumns + Col] = Pvalue;
  }
}
void checkResult(float *array_A,float *array_B,int size){
    double epsilon = 1.0E-8;
    for(int i=0;i<size;i++){
        if(abs(array_A[i] - array_B[i]) > epsilon){
            printf("Error! Matrix[%05d]:%0.8f != %0.8f\n",i,array_A[i],array_B[i]);
            return;
        }
    }
    printf("Check result success!\n");
}

int main(){
    float *array_A,*array_B,*array_C,*array_C_host; 
    float *d_arrayA,*d_arrayB,*d_arrayC;
    int size_A = M * K * sizeof(float);
    int size_B = K * N * sizeof(float);
    int size_C = M * N * sizeof(float);
    array_A = (float*)malloc(size_A);
    array_B = (float*)malloc(size_B);
    array_C = (float*)malloc(size_C);
    array_C_host = (float*)malloc(size_C);
    initial(array_A,M * K);
    initial(array_B,K * N);
    //printMatrix(array_A,M,K);
    //printMatrix(array_B,K,N);
    struct timeval start,end;
    gettimeofday(&start,NULL);
    multiplicateMatrixonHost(array_A,array_B,array_C_host,M,K,N);
    gettimeofday(&end,NULL);
    cout<<"CPU time:"<<1000000 * (end.tv_sec - start.tv_sec) + end.tv_usec - start.tv_usec<<"us"<<endl;
    cudaMalloc((void**)&d_arrayA,size_A);
    cudaMalloc((void**)&d_arrayB,size_B);
    cudaMalloc((void**)&d_arrayC,size_C);
    cudaMemcpy(d_arrayA,array_A,size_A,cudaMemcpyHostToDevice);
    cudaMemcpy(d_arrayB,array_B,size_B,cudaMemcpyHostToDevice);
    dim3 dimBlock(BLOCK_SIZE,BLOCK_SIZE);
    dim3 dimGrid((N - 1)/ dimBlock.x + 1,(M - 1) / dimBlock.y + 1);
    gettimeofday(&start,NULL);
    matrixMultiplyShared<<<dimGrid,dimBlock>>>(d_arrayA,d_arrayB,d_arrayC,M,K,K,N,M,N);
    gettimeofday(&end,NULL);
    cout<<"GPU time:"<<1000000 * (end.tv_sec - start.tv_sec) + end.tv_usec - start.tv_usec<<"us"<<endl;
    cudaMemcpy(array_C,d_arrayC,size_C,cudaMemcpyDeviceToHost);
    checkResult(array_C,array_C_host,M * N);
    //cout<<"Array C is :"<<endl;
    //printMatrix(array_C,M,N);
    cudaFree(d_arrayA);
    cudaFree(d_arrayB);
    cudaFree(d_arrayC);
    free(array_A);
    free(array_B);
    free(array_C);
    free(array_C_host);
    return 0;
}

在这里,我着重解释一下matrixMultiplyShared函数。
这是一个利用共享内存计算矩阵乘法的 CUDA 核函数。我将逐行解释代码并在其中的 for 循环部分举一个例子。

global void matrixMultiplyShared(float *A, float *B, float *C, int numARows,
int numAColumns, int numBRows,
int numBColumns, int numCRows,
int numCColumns) {
这是一个全局函数,用于定义 CUDA 核函数。它接收两个输入矩阵 A 和 B,一个输出矩阵 C,以及与这些矩阵相关的尺寸参数。

shared float ds_M[BLOCK_SIZE][BLOCK_SIZE];
shared float ds_N[BLOCK_SIZE][BLOCK_SIZE];
定义两个共享内存数组,用于存储来自矩阵 A 和 B 的局部数据。每个数组的尺寸为 BLOCK_SIZE x BLOCK_SIZE。

int bx = blockIdx.x;
int by = blockIdx.y;
int tx = threadIdx.x;
int ty = threadIdx.y;
获取当前线程所属的线程块的索引(bx,by)和线程在线程块内的索引(tx,ty)。

int Row = by * BLOCK_SIZE + ty;
int Col = bx * BLOCK_SIZE + tx;
计算当前线程所负责的输出矩阵 C 的元素的行(Row)和列(Col)。
float Pvalue = 0;
初始化累积变量 Pvalue,用于存储计算过程中的中间结果。

for (int m = 0; m < (numAColumns - 1) / BLOCK_SIZE + 1; ++m) {
外层循环用于遍历矩阵 A 和 B 的局部块。m 的上限保证了遍历所有需要的局部块。

if (Row < numARows && m * BLOCK_SIZE + tx < numAColumns) {
  ds_M[ty][tx] = A[Row * numAColumns + m * BLOCK_SIZE + tx];
} else {
  ds_M[ty][tx] = 0.0;
}

将矩阵 A 的局部数据加载到共享内存数组 ds_M。如果当前线程所负责的元素位于矩阵 A 的有效范围内,则从矩阵 A 中读取对应元素;否则,将 ds_M 中对应位置设为 0.0。

if (Col < numBColumns && m * BLOCK_SIZE + ty < numBRows) {
  ds_N[ty][tx] = B[(m * BLOCK_SIZE + ty) * numBColumns + Col];
} else {
  ds_N[ty][tx] = 0.0;
}

中读取对应元素;否则,将 ds_N 中对应位置设为 0.0。

cpp
Copy code
__syncthreads();
同步线程,确保所有线程已完成从矩阵 A 和 B 中加载数据到共享内存。

cpp
Copy code
for (int k = 0; k < BLOCK_SIZE; ++k) {
Pvalue += ds_M[ty][k] * ds_N[k][tx];
}
内层循环执行局部矩阵乘法。遍历当前线程负责的共享内存 ds_M 的一行和 ds_N 的一列,执行点乘并将结果累加到 Pvalue。

举个例子,假设 BLOCK_SIZE 为 2,那么 ds_M 和 ds_N 的尺寸都是 2x2。假设当前线程负责计算输出矩阵 C 的第一行第一列元素,即 Row 为 0,Col 为 0。那么 ty 和 tx 都是 0。在内层循环中,我们执行以下操作:

k = 0: Pvalue += ds_M[0][0] * ds_N[0][0]
k = 1: Pvalue += ds_M[0][1] * ds_N[1][0]
这样,Pvalue 就包含了 ds_M 第一行与 ds_N 第一列的点乘结果。

__syncthreads();

再次同步线程,确保所有线程已完成局部矩阵乘法,以便进行下一轮迭代。

结束外层循环。此时 Pvalue 包含了当前线程负责的输出矩阵 C 的元素所需的所有局部矩阵乘法结果。

if (Row < numCRows && Col < numCColumns) {
C[Row * numCColumns + Col] = Pvalue;
}
将 Pvalue 写入输出矩阵 C 的相应位置。如果当前线程负责的元素位于矩阵 C 的有效范围内,则将 Pvalue 写入矩阵 C。

核函数结束。

以上就是对这个并行矩阵乘法核函数的逐行解释。核心思想是将矩阵 A 和 B 分为大小为 BLOCK_SIZE x BLOCK_SIZE 的局部块,利用共享内存存储这些局部数据,然后利用线程计算局部矩阵乘法并累加结果。这种方法充分利用了 GPU 的内存层次结构,提高了计算性能。

DS_M 和 DS_N的索引方式解释

为什么在这里DS_M 和 DS_N是以这种方式进行索引的呢?

if (Row < numARows && m * BLOCK_SIZE + tx < numAColumns) {
      ds_M[ty][tx] = A[Row * numAColumns + m * BLOCK_SIZE + tx];

  if (Col < numBColumns && m * BLOCK_SIZE + ty < numBRows) {
      ds_N[ty][tx] = B[(m * BLOCK_SIZE + ty) * numBColumns + Col];
    } 

这是因为矩阵 A 的元素被加载到共享内存 ds_M 中,而矩阵 B 的元素被加载到共享内存 ds_N 中。加载矩阵 A 和 B 的元素时,需要考虑到它们的不同形状以及线程和块的布局。

对于 ds_M,我们将行索引 Row 与 ty 相关联,将列索引与 m * BLOCK_SIZE + tx 相关联。以确保在加载 A 的元素时,每个线程可以从 A 中获取与其对应的元素。例如,假设 A 是一个 4x4 的矩阵,BLOCK_SIZE 是 2。那么,线程布局将如下所示:

A:
| 0 1 2 3 |
| 4 5 6 7 |
| 8 9 10 11 |
|12 13 14 15 |

Thread layout (ty, tx):
| (0, 0) (0, 1) |
| (1, 0) (1, 1) |

根据 ds_M 的索引方式,可以看到每个线程将加载 A 的元素,如下所示:

Thread (0, 0): A[0] (Row = 0, m = 0, tx = 0)
Thread (0, 1): A[1] (Row = 0, m = 0, tx = 1)
Thread (1, 0): A[4] (Row = 1, m = 0, tx = 0)
Thread (1, 1): A[5] (Row = 1, m = 0, tx = 1)
对于 ds_N,我们将行索引与 m * BLOCK_SIZE + ty 相关联,将列索引 Col 与 tx 相关联。这确保了在加载 B 的元素时,每个线程可以从 B 中获取与其对应的元素。例如,假设 B 也是一个 4x4 的矩阵,BLOCK_SIZE 是 2。那么线程布局将如下所示:

B:
| 0 1 2 3 |
| 4 5 6 7 |
| 8 9 10 11 |
|12 13 14 15 |

Thread layout (ty, tx):
| (0, 0) (0, 1) |
| (1, 0) (1, 1) |

根据 ds_N 的索引方式,可以看到每个线程将加载 B 的元素,如下所示:

Thread (0, 0): B[0] (Col = 0, m = 0, ty = 0)
Thread (0, 1): B[1] (Col = 1, m = 0, ty = 0)
Thread (1, 0): B[4] (Col = 0, m = 0, ty = 1)
Thread (1, 1): B[5] (Col = 1, m = 0, ty = 1)
通过这种方式,我们可以确保在计算局部矩阵乘法时,每个线程都可以从共享内存 ds_M 和 ds_N 中获取所需的元素。现在我们来计算局部矩阵乘法:

在这个循环中:
for (int k = 0; k < BLOCK_SIZE; ++k) {
Pvalue += ds_M[ty][k] * ds_N[k][tx];
}

每个线程计算一个 Pvalue,它是矩阵 A 的一行与矩阵 B 的一列的点积。这是一个局部矩阵乘法,因为我们在每次循环迭代时只计算 BLOCK_SIZE 大小的子矩阵。

以一个具体的例子来说明这个过程。假设我们有以下两个矩阵 A 和 B:
A: B:
| 1 2 | | 5 6 |
| 3 4 | | 7 8 |
BLOCK_SIZE 为 2,所以我们的线程布局如下:
Thread layout (ty, tx):
| (0, 0) (0, 1) |
| (1, 0) (1, 1) |

对于每个线程,我们将计算以下点积:

Thread (0, 0): Pvalue = ds_M[0][0] * ds_N[0][0] + ds_M[0][1] * ds_N[1][0] = 1 * 5 + 2 * 7 = 19
Thread (0, 1): Pvalue = ds_M[0][0] * ds_N[0][1] + ds_M[0][1] * ds_N[1][1] = 1 * 6 + 2 * 8 = 22
Thread (1, 0): Pvalue = ds_M[1][0] * ds_N[0][0] + ds_M[1][1] * ds_N[1][0] = 3 * 5 + 4 * 7 = 43
Thread (1, 1): Pvalue = ds_M[1][0] * ds_N[0][1] + ds_M[1][1] * ds_N[1][1] = 3 * 6 + 4 * 8 = 50
所以最终的结果矩阵 C 为:
C:
| 19 22 |
| 43 50 |


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/488517.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JeecgBoot低代码平台—默认模糊查询以及高级查询规则

JeecgBoot低代码开发平台&#xff0c;自己封装了一套查询过滤器&#xff0c;默认就支持模糊查询&#xff0c;只是需要前后加上*&#xff0c;虽然麻烦&#xff0c;但是这样是考虑到系统后期数据量大默认模糊查询会导致系统性能问题。当然如果你的系统数据量级别达不到这个情况&a…

高校大数据实验室解决方案

高校实验室建设总目标是搭建一站式教学服务平台&#xff0c;一站式教学服务平台概念是在深刻理解高校学科建设及存在的若干问题上提出。围绕着学科建设的各个方面&#xff0c;从专业开设、课程设置、师资培养、教学资源、实验环境、学生实训实习及就业创业等环节&#xff0c;提…

Android热修复原理与实战

作者&#xff1a;独孤狼 什么是热修复 在我们应用上线后出现bug需要及时修复时&#xff0c;不用再发新的安装包&#xff0c;只需要发布补丁包&#xff0c;在客户无感知下修复掉bug 怎么进行热修复 服务端&#xff1a;补丁包管理 用户端&#xff1a;执行热修复 开发端&#xff1…

Introduction to modern Cryptography 现代密码学原理与协议第一章笔记

加密的语法由三个算法组成:密钥产生&#xff0c;加密&#xff0c;解密 (1) 密钥产生算法Gen是一个概率算法&#xff0c;能够根据方案定义的某种分布方案分布选择并输出一个密钥k (2) 加密算法Enc,输入为密钥k和明文m&#xff0c;输出为密文c。把使用密钥k加密明文m记为Enck(m) …

【Vue 基础】尚品汇项目-06-vuex模块式开发

vuex是官方提供一个插件&#xff0c;状态管理库&#xff0c;集中式管理项目中组件共用的数据。 目录 一、安装 二、vuex的基本使用 三、vuex模块式开发 一、安装 安装命令&#xff1a; npm install vuex3.6.2 --save 如果安装错版本&#xff0c;要先卸载再重新安装&#…

认识Filter(过滤器)

Filter介绍 在计算机编程中&#xff0c;Filter&#xff08;过滤器&#xff09;是一种用于对数据流进行处理的软件组件。Filter 的作用是从输入流中获取数据&#xff0c;对其进行处理后再将其写入输出流中。Filter 组件通常用于数据校验、数据转换、数据压缩等方面&#xff0c;…

微服务知识2

CAP和BASE是分布式必备理论基础 CAP理论 一致性(C)&#xff1a;写操作之后进行读操作无论在哪个节点都需要返回写操作的值 可用性(A)&#xff1a;非故障的节点在合理的时间内返回合理的响应 分区容错性(P)&#xff1a;当出现网络分区后&#xff0c;系统能够继续工作&#x…

家用平价洗地机哪款好?国产性价比高的品牌

在当今社会&#xff0c;人们使用清洁电器已经非常普及&#xff0c;成为了人们日常清洁中必不可少的得力助手了。洗地机在我看来&#xff0c;它在清洁力度上做的十分优秀&#xff0c;无论是干湿垃圾还是顽固污渍&#xff0c;皆可以清洗到位&#xff1b;同时&#xff0c;洗地机配…

《花雕学AI》AI 人工智能伙伴关系的指南:遵循原则,实现实践,展望未来

引言&#xff1a;人工智能&#xff08;AI&#xff09;是指由人造的机器或系统所展现出的智能&#xff0c;它可以模拟或扩展人类的认知功能&#xff0c;如学习、推理、感知、交流等。 人工智能的发展和应用已经深刻地影响了社会、经济、文化和政治等各个领域&#xff0c;同时也带…

( 数组和矩阵) 697. 数组的度 ——【Leetcode每日一题】

❓697. 数组的度 难度&#xff1a;简单 给定一个非空且只包含非负数的整数数组 nums&#xff0c;数组的 度 的定义是指数组里任一元素出现频数的最大值。 你的任务是在 nums 中找到与 nums 拥有相同大小的度的最短连续子数组&#xff0c;返回其长度。 示例 1&#xff1a; 输…

Java异常处理传递规范总结

java 异常分类 Thorwable类&#xff08;表示可抛出&#xff09;是所有异常和错误的超类&#xff0c;两个直接子类为Error和Exception&#xff0c;分别表示错误和异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常&#xff0c; 这两种异常有很大的区别…

Linux 概述及常用命令(一)

1、Linux 简介 Linux 是一套免费使用和自由传播的类 Unix 操作系统&#xff08;主要用在服务器上&#xff09;&#xff0c;是一个基于 POSIX 和 Unix 的多用户、多任务、支持多线程和多 CPU 的操作系统。 目前市面上较知名的发行版有&#xff1a;Ubuntu&#xff0c;RedHat&…

大学生创业者最关心的问题——校园外卖到底能不能盈利?

与前十年的寒窗苦读相比&#xff0c;大学自由散漫的生活略显空虚。除了学习&#xff0c;学生们还有很多时间自由安排。有些人选择颓废&#xff0c;有些人选择勤奋。而校园创业是许多有想法的大学生都会做的事情。其中&#xff0c;外卖跑腿配送是特别受欢迎的创业项目之一。 那…

五月到了,再来看看ChatGPT给我们带来了什么吧!

ChatGPT&#xff0c;即聊天生成预训练转换器&#xff08;Chat Generative Pre-trained Transformer&#xff09;&#xff0c;是美国OpenAI公司基于GPT-3.5架构&#xff08;目前已经更新到GPT-4&#xff0c;但仅限于Plus用户&#xff09;研发和强化训练的一款人工智能聊天机器人…

C++学习day--04 图形化开发环境搭建

1、为什么要搭建图形化开发环境 因为很多初学者学习C就是单纯的控制台打印输出&#xff0c;很枯燥&#xff0c;我们今后在做项目或者在学习的过程中&#xff0c;采用图形化方式相结合的方法学习&#xff0c;再学习中体会编程带来的快乐。先说明一下&#xff0c;奇牛课程是边学…

YOLOv6 4.0 使用记录:python推理 OpenCV DNN 推理

目录 1、下载源码 2、下载权重文件 3、配置环境 4、推理 6、ONNX格式导出 权重文件为yolov6list_s.pt 权重为yolov6.pt 7、opencv DNN推理 8、个人总结 1、下载源码 下载最新的4.0版本的 2、下载权重文件 我下的是YOLOv6Lite-S 3、配置环境 cd到项目目录&#xff0c;运…

关于IDEA编译运行时出现 《非法字符: ‘\ufeff‘ 需要class,interface或enum》的解决办法

问题如下 原因&#xff1a;编码问题解决办法&#xff1a;将这些报错的文件拷贝到一个目录中&#xff0c;然后用notpad打开&#xff0c;点击工具类上的 编码&#xff0c;将 以uft8格式编码 转为 以utf8无BOM格式编码&#xff0c;然后保存&#xff0c;再覆盖idea中对应的文件。或…

人脸识别--传统+深度方法

人脸识别算法--非深度方法 在前深度学习时代&#xff0c;非深度的方法探索了不同的人脸识别算法。 先考虑一下非深度学习时代&#xff0c;人脸识别难在哪&#xff1f;或者说目标识别的难点在哪&#xff1f; 图像是一个高度冗余的数据。 * 图像数据中包含大量与语义无关的内容…

Hadoop3.3.1 Windows环境配置

1.解压hadoop-3.3.1.tar.gz 到windows安装路径 D:\h3\hadoop-3.3.1 2.检查Windows jdk是否安装正常 3.下载winutils GitHub - steveloughran/winutils: Windows binaries for Hadoop versions (built from the git commit ID used for the ASF relase) 解压到D:\h3\ 并重命名…

Python每日一练(20230504) 课程表 Course Schedule I/II

目录 1. 课程表 Course Schedule I 2. 课程表 Course Schedule II &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 课程表 Course Schedule I 你这个学期必须选修 numCourses 门课程&a…