现代C++中的从头开始深度学习【2/8】:张量编程

一、说明

        初学者文本:此文本需要入门级编程背景和对机器学习的基本了解。张量是在深度学习算法中表示数据的主要方式。它们广泛用于在算法执行期间实现输入、输出、参数和内部状态。

        在这个故事中,我们将学习如何使用特征张量 API 来开发我们的C++算法。具体来说,我们将讨论:

  • 什么是张量
  • 如何在C++中定义张量
  • 如何计算张量运算
  • 张量约简和卷积

        在本文的最后,我们将实现 Softmax 作为将张量应用于深度学习算法的说明性示例。

二、什么是张量?

张量是类似网格的数据结构,它概括了任意数量的轴的向量和矩阵的概念。在机器学习中,我们通常使用“维度”这个词而不是“轴”。张量不同维度的数量也称为张量

不同秩张量

在实践中,我们使用张量来表示算法中的数据,并用它们执行算术运算。

我们可以用张量执行的更简单的操作是所谓的元素级操作:给定两个具有相同维度的操作数张量,该操作会产生一个具有相同维度的新张量,其中每个系数的值是从操作数中各个元素的二进制评估中获得的:

系数乘法

上面的例子是两个 2 秩张量的系数乘积的图示。此操作对任何两个张量仍然有效,因为它们具有相同的维度。

像矩阵一样,我们可以使用张量执行其他更复杂的操作,例如矩阵类积、卷积、收缩、约简和无数的几何运算。在这个故事中,我们将学习如何使用特征张量 API 来执行其中一些张量操作,重点介绍对深度学习算法实现最重要的操作。

三、如何在C++中声明和使用张量

        众所周知,本征是一个广泛用于矩阵计算的线性代数库。除了众所周知的对矩阵的支持之外,Eigen 还有一个(不支持的)张量模块。

虽然 Eigen Tensor API 表示不受支持,但它实际上得到了 Google TensorFlow 框架开发人员的良好支持。

        我们可以使用特征轻松定义张量:

#include <iostream>#include <unsupported/Eigen/CXX11/Tensor>int main(int, char **)
{Eigen::Tensor<int, 3> my_tensor(2, 3, 4);my_tensor.setConstant(42);std::cout << "my_tensor:\n\n" << my_tensor << "\n\n";std::cout << "tensor size is " << my_tensor.size() << "\n\n"; return 0;
}

该行

Eigen::Tensor<int, 3> my_tensor(2, 3, 4);

创建一个张量对象并分配存储整数所需的内存。在此示例中,是一个 3 秩张量,其中第一维的大小为 2,第二维的大小为 3,最后一维的大小为 4。我们可以表示如下:2x3x4my_tensormy_tensor

如果需要,我们可以设置张量数据:

my_tensor.setValues({{{1, 2, 3, 4}, {5, 6, 7, 8}}});std::cout << "my_tensor:\n\n" << my_tensor << "\n\n";

或改用随机值。例如,我们可以做:

Eigen::Tensor<float, 2> kernel(3, 3);
kernel.setRandom();
std::cout << "kernel:\n\n" << kernel << "\n\n";

        并在以后使用此内核来执行卷积。我们将很快在这个故事中介绍卷积。首先,让我们学习如何使用TensorMaps。

四、使用 Eigen::TensorMap 创建张量视图

有时,我们分配了一些数据,只想使用张量来操作它。 类似于 但是,它不是分配新数据,而只是作为参数传递的数据的视图。检查以下示例:Eigen::TensorMapEigen::Tensor

//an vector with size 12
std::vector<float> storage(4*3);// filling vector from 1 to 12
std::iota(storage.begin(), storage.end(), 1.);for (float v: storage) std::cout << v << ','; 
std::cout << "\n\n";// setting a tensor view with 4 rows and 3 columns
Eigen::TensorMap<Eigen::Tensor<float, 2>> my_tensor_view(storage.data(), 4, 3);std::cout << "my_tensor_view before update:\n\n" << my_tensor_view << "\n\n";// updating the vector
storage[4] = -1.;std::cout << "my_tensor_view after update:\n\n" << my_tensor_view << "\n\n";// updating the tensor
my_tensor_view(2, 1) = -8;std::cout << "vector after two updates:\n\n";
for (float v: storage) std::cout << v << ','; 
std::cout << "\n\n";

在这个例子中,很容易看出(默认情况下)特征张量 API 中的张量是 col-major。col-major和row-major是指网格数据如何存储在线性容器中的方式(查看维基百科上的这篇文章):

虽然我们可以使用行大张量,但不建议这样做:

目前仅完全支持默认列主布局,因此目前不建议尝试使用行主布局。

Eigen::TensorMap非常有用,因为我们可以使用它来节省内存,这对于深度学习算法等高要求的应用程序至关重要。

五、执行一元和二进制操作

        特征张量 API 定义了常见的算术重载运算符,这使得对张量进行编程非常直观和直接。例如,我们可以加减张量:

Eigen::Tensor<float, 2> A(2, 3), B(2, 3);
A.setRandom();
B.setRandom();Eigen::Tensor<float, 2> C = 2.f*A + B.exp();std::cout << "A is\n\n"<< A << "\n\n";
std::cout << "B is\n\n"<< B << "\n\n";
std::cout << "C is\n\n"<< C << "\n\n";

特征张量 API 还有其他几个元素级函数,如 、 和 。此外,我们可以按如下方式使用:.exp()sqrt()log()abs()unaryExpr(fun)

auto cosine = [](float v) {return cos(v);};
Eigen::Tensor<float, 2> D = A.unaryExpr(cosine);
std::cout << "D is\n\n"<< D << "\n\n";

同样,我们可以使用:binaryExpr

auto fun = [](float a, float b) {return 2.*a + b;};
Eigen::Tensor<float, 2> E = A.binaryExpr(B, fun);
std::cout << "E is\n\n"<< E << "\n\n";

六、惰性求值和 auto 关键字

开发Eigen Tensor API的Google工程师遵循了与Eigen库顶部相同的策略。这些策略之一,也可能是最重要的策略,是如何延迟计算表达式的方式。

惰性求值策略包括延迟表达式的实际求值,以便将多个链式表达式组合到一个优化的等效表达式中。因此,优化的代码不是逐步计算多个单独的表达式,而是只计算一个表达式,旨在利用最终的整体性能。

例如,如果 和 是张量,则表达式实际上并不计算 A 和 B 的总和。实际上,该表达式会产生一个知道如何计算的特殊对象。仅当将此特殊对象分配给实际张量时,才会执行实际操作。换句话说,在下面的语句中:ABA + BA + BA + B

auto C = A + B;

C不是实际结果,而只是一个知道如何计算的计算对象(确实是一个对象)。只有当分配给张量对象(类型、、等的对象)时,才会对其进行评估以提供正确的张量值:A + BEigen::TensorCwiseBinaryOpA + BCEigen::TensorEigen::TensorMapEigen::TensorRef

Eigen::Tensor<...> T = C;
std::cout << "T is " << T << "\n\n";

当然,这对于像 这样的小型操作没有意义。但是,此行为对于长操作链非常有用,在这些操作链中,可以在实际评估之前优化计算。在简历中,作为一般准则,而不是编写这样的代码:A + B

Eigen::Tensor<...> A = ...;
Eigen::Tensor<...> B = ...;
Eigen::Tensor<...> C = B * 0.5f;
Eigen::Tensor<...> D = A + C;
Eigen::Tensor<...> E = D.sqrt();

我们应该编写这样的代码:

Eigen::Tensor<...> A = ...;
Eigen::Tensor<...> B = ...;
auto C = B * 0.5f;
auto D = A + C;
Eigen::Tensor<...> E = D.sqrt();

不同之处在于,在前者中,实际上是对象,而在后面的代码中,它们只是惰性计算操作。CDEigen::Tensor

在恢复中,最好使用惰性计算来评估长操作链,因为该链将在内部进行优化,最终导致更快的执行。

七、几何运算

几何运算会产生具有不同维度的张量,有时还会产生大小。这些操作的示例包括:、、、 和 。reshapepadshufflestridebroadcast

值得注意的是,特征张量 API 没有操作。不过,我们可以使用以下方法进行模拟:transposetransposeshuffle

auto transpose(const Eigen::Tensor<float, 2> &tensor) {Eigen::array<int, 2> dims({1, 0});return tensor.shuffle(dims);
}Eigen::Tensor<float, 2> a_tensor(3, 4);
a_tensor.setRandom();std::cout << "a_tensor is\n\n"<< a_tensor << "\n\n";
std::cout << "a_tensor transpose is\n\n"<< transpose(a_tensor) << "\n\n";

稍后,当我们讨论使用张量的示例时,我们将看到一些几何运算的示例。softmax

八、规约(reduce)

        归约是一种特殊操作情况,它会导致张量的维数低于原始张量。减少的直观案例是:sum()maximum()

Eigen::Tensor<float, 3> X(5, 2, 3);
X.setRandom();std::cout << "X is\n\n"<< X << "\n\n";std::cout << "X.sum(): " << X.sum() << "\n\n";
std::cout << "X.maximum(): " << X.maximum() << "\n\n";

在上面的示例中,我们缩小了所有尺寸一次。我们还可以沿特定轴执行缩减。例如:

Eigen::array<int, 2> dims({1, 2});std::cout << "X.sum(dims): " << X.sum(dims) << "\n\n";
std::cout << "X.maximum(dims): " << X.maximum(dims) << "\n\n";

        特征张量 API 具有一组预构建的归约操作,例如、、、等。如果任何预构建的操作不适合特定实现,我们可以使用提供自定义函子作为参数。prodanyallmeanreduce(dims, reducer)reducer

九、张量卷积

        在前面的一个故事中,我们学习了如何仅使用普通C++和特征矩阵来实现 2D 卷积。事实上,这是必要的,因为在本征矩阵中没有内置的矩阵卷积。幸运的是,特征张量 API 有一个方便的函数来对特征张量对象执行卷积:

Eigen::Tensor<float, 4> input(1, 6, 6, 3);
input.setRandom();Eigen::Tensor<float, 2> kernel(3, 3);
kernel.setRandom();Eigen::Tensor<float, 4> output(1, 4, 4, 3);Eigen::array<int, 2> dims({1, 2});
output = input.convolve(kernel, dims);std::cout << "input:\n\n" << input << "\n\n";
std::cout << "kernel:\n\n" << kernel << "\n\n";
std::cout << "output:\n\n" << output << "\n\n";

请注意,我们可以通过控制卷积中幻灯片的尺寸来执行 2D、3D、4D 等卷积。

十、带张量的软最大值

        在编程深度学习模型时,我们使用张量而不是矩阵。事实证明,矩阵可以表示一个或最多二维网格,同时我们有更高维度的数据多通道图像或批量寄存器来处理。这就是张量发挥作用的地方。

        让我们考虑以下示例,其中我们有两批寄存器,每批有 4 个寄存器,每个寄存器有 3 个值:

        我们可以按如下方式表示这些数据:

Eigen::Tensor<float, 3> input(2, 4, 3);
input.setValues({{{0.1, 1., -2.},{10., 2., 5.},{5., -5., 0.},{2., 3., 2.}},{{100., 1000., -500.},{3., 3., 3.},{-1, 1., -1.},{-11., -0.2, -.1}}
});std::cout << "input:\n\n" << input << "\n\n";

        现在,让我们应用于此数据:softmax

Eigen::Tensor<float, 3> output = softmax(input);
std::cout << "output:\n\n" << output << "\n\n";

        Softmax是一种流行的激活功能。我们在上一个故事中介绍了它的实现。现在,让我们介绍一下实现:Eigen::MatrixEigen::Tensor

#include <unsupported/Eigen/CXX11/Tensor>auto softmax(const Eigen::Tensor<float, 3> &z)
{auto dimensions = z.dimensions();int batches = dimensions.at(0);int instances_per_batch = dimensions.at(1);int instance_length = dimensions.at(2);Eigen::array<int, 1> depth_dim({2});auto z_max = z.maximum(depth_dim);Eigen::array<int, 3> reshape_dim({batches, instances_per_batch, 1});auto max_reshaped = z_max.reshape(reshape_dim);Eigen::array<int, 3> bcast({1, 1, instance_length});auto max_values = max_reshaped.broadcast(bcast);auto diff = z - max_values;auto expo = diff.exp();auto expo_sums = expo.sum(depth_dim);auto sums_reshaped = expo_sums.reshape(reshape_dim);auto sums = sums_reshaped.broadcast(bcast);auto result = expo / sums;return result;
}

        此代码输出:

        我们不会在这里详细介绍 Softmax。如果您需要查看Softmax算法,请不要犹豫,在Medium上再次阅读之前的故事。现在,我们只专注于了解如何使用特征张量来编码我们的深度学习模型。

        首先要注意的是,该函数实际上并没有计算参数的softmax值。实际上,只挂载一个可以计算softmax的复杂对象。softmax(z)zsoftmax(z)

        仅当 的结果分配给类似张量的对象时,才会评估实际值。例如,在这里:softmax(z)

Eigen::Tensor<float, 3> output = softmax(input);

        在这一行之前,一切都只是softmax的计算图,希望得到优化。发生这种情况只是因为我们在 的正文中使用了关键字。因此,特征张量 API 可以优化使用更少操作的整个计算,从而改善处理和内存使用。autosoftmax(z)softmax(z)

        在结束这个故事之前,我想指出和呼吁:tensor.reshape(dims)tensor.broadcast(bcast)

Eigen::array<int, 3> reshape_dim({batches, instances_per_batch, 1});
auto max_reshaped = z_max.reshape(reshape_dim);Eigen::array<int, 3> bcast({1, 1, instance_length});
auto max_values = max_reshaped.broadcast(bcast);

  reshape(dims)是一种特殊的几何运算,它生成另一个张量,其大小与原始张量相同,但尺寸不同。重塑不会在张量内部更改数据的顺序。例如:

Eigen::Tensor<float, 2> X(2, 3);
X.setValues({{1,2,3},{4,5,6}});std::cout << "X is\n\n"<< X << "\n\n";std::cout << "Size of X is "<< X.size() << "\n\n";Eigen::array<int, 3> new_dims({3,1,2});
Eigen::Tensor<float, 3> Y = X.reshape(new_dims);std::cout << "Y is\n\n"<< Y << "\n\n";std::cout << "Size of Y is "<< Y.size() << "\n\n";

Note that, in this example, the size of X and Y is either 6 although they have very different geometry.

tensor.broadcast(bcast) repeats the tensor as many times as provided in the parameter for each dimension. For example:bcast

Eigen::Tensor<float, 2> Z(1,3);
Z.setValues({{1,2,3}});
Eigen::array<int, 2> bcast({4, 2});
Eigen::Tensor<float, 2> W = Z.broadcast(bcast);std::cout << "Z is\n\n"<< Z << "\n\n";
std::cout << "W is\n\n"<< W << "\n\n";

不同的 ,不会改变张量秩(即维数),而只会增加维数的大小。reshapebroadcast

十一、局限性

特征张量 API 文档引用了一些我们可以意识到的限制:

  • GPU 支持经过测试并针对浮点类型进行了优化。即使我们可以声明,在使用 GPU 时也不鼓励使用非浮点张量。Eigen::Tensor<int,...> tensor;
  • 默认布局(col-major)是唯一实际支持的布局。至少现在我们不应该使用行专业。
  • 最大尺寸数为 250。只有在使用 C++11 兼容的编译器时才能实现此大小。

十二、结论和下一步

        张量是机器学习编程的基本数据结构,使我们能够像使用常规二维矩阵一样直接地表示和处理多维数据。

在这个故事中,我们介绍了特征张量 API,并学习了如何相对轻松地使用张量。我们还了解到,特征张量 API 具有惰性评估机制,可以在内存和处理时间方面优化执行。

为了确保我们真正理解Eigen Tensor API的用法,我们介绍了一个使用张量编码Softmax的示例。

在接下来的故事中,我们将继续使用 C++ 和特征从头开始开发高性能深度学习算法,特别是使用 Eigen Tensor API。

十三、github代码

您可以在 GitHub 上的此存储库中找到此故事中使用的代码。

十四、引用

[1] 特征张量 API

[2] 特征张量模块

[3] Eigen Gitlab repository, libeigen / eigen · GitLab

[4] Charu C. Aggarwal, Neural Networks and Deep Learning: A Textbook (2018), Springer

[5] Jason Brownlee,A Gentle Introduction to Tensors for Machine Learning with NumPy

关于本系列

在本系列中,我们将学习如何仅使用普通和现代C++对必须知道的深度学习算法进行编码,例如卷积、反向传播、激活函数、优化器、深度神经网络等。

这个故事是:使用特征张量API

查看其他故事:

0 — 现代C++深度学习编程基础

1 — 在纯C++中编码 2D 卷积

2 — 使用 Lambda 的成本函数

3 — 实现梯度下降

4 — 激活函数

...更多内容即将推出。

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

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

相关文章

20230806将ASF格式的视频转换为MP4

20230806将ASF格式的视频转换为MP4 2023/8/6 18:47 缘起&#xff0c;自考中山大学的《计算机网络》&#xff0c;考试《数据库系统原理》的时候找到视频&#xff0c;由于个人的原因&#xff0c;使用字幕更加有学习效率&#xff01; 由于【重型】的PR2023占用资源较多&#xff0c…

设计模式——单例模式(懒汉和饿汉)

单例模式 一、概念 单例模式是一种对象创建型模式&#xff0c;使用单例模式&#xff0c;可以保证为一个类只生成唯一的实例对象。也就是说&#xff0c;在整个程序空间中&#xff0c;该类只存在一个实例对象。一个类只能有一个实例在生活中是很常见的&#xff0c;比如打印机程…

Python 如何获取图片中的文字----OCR安装使用

环境: windows10, anaconda3 背景&#xff1a;使用conda install pytesseract 安装失败 解决方法&#xff1a; 从Index of /tesseract (uni-mannheim.de)中下载最新的安装包 https://digi.bib.uni-mannheim.de/tesseract/tesseract-ocr-w64-setup-5.3.1.20230401.exe 点击…

【Linux】冯诺伊曼体系结构|操作系统概念理解

个人主页&#xff1a;&#x1f35d;在肯德基吃麻辣烫 我的gitee&#xff1a;Linux仓库 个人专栏&#xff1a;Linux专栏 分享一句喜欢的话&#xff1a;热烈的火焰&#xff0c;冰封在最沉默的火山深处 文章目录 前言一、先谈硬件——冯诺依曼体系结构1.什么是冯诺依曼体系结构&am…

「文本隐写」相关会议期刊投稿注意事项

前言&#xff1a;本篇博客总结「文本隐写」研究领域相关会议与期刊的投稿注意事项。 目录 一、会议ACL (The Annual Meeting of the Association for Computational Linguistics)IH&MMSec (The ACM Workshop on Information Hiding, Multimedia and Security)CCS (The ACM …

Centos7.9_解决每次重启机器配置的java环境变量都需要重新source /etc/profile才生效的问题---Linux工作笔记060

这种情况需要把环境变量,java的环境变量在/root/.bashrc文件中也放一份,注意这个文件是隐藏的,默认是,需要进行ls -a才能显示. #jdk export JAVA_HOME/lib/jvm export JRE_HOME${JAVA_HOME}/jre export CLASSPATH.:${JAVA_HOME}/lib:${JRE_HOME}/lib export PATH${JAVA_HOME}/b…

【学习FreeRTOS】第3章——FreeRTOS移植及配置文件

1.FreeRTOS源码简介 【一级目录&#xff1a;/】以下FreeRTOS的源码&#xff0c;其中&#xff0c;FreeRTOS文件夹最为重要&#xff0c;代笔FreeRTOS内核 【二级目录&#xff1a;/FreeRTOS】以下为FreeRTOS文件夹的内容&#xff0c;比较重要的有Demo文件夹和Source文件夹 【三级…

兰州https证书申请

https证书是由CA认证机构颁发的数字证书&#xff0c;可以为域名网站或者公网IP网站提供信息加密服务&#xff0c;正规CA认证机构签发的https证书可以兼容99%的主流浏览器和IOS、Windows系统&#xff0c;同样&#xff0c;现在流行的小程序也需要https证书。那么&#xff0c;该怎…

语音秘书:让录音转文字识别软件成为你的智能工作助手

每当在需要写文章的深夜&#xff0c;我的思绪经常跟不上我的笔&#xff0c;即便是说出来用录音机录下&#xff0c;再书写出来&#xff0c;也需要耗费大量时间。这个困扰了我很久的问题终于有了解决的办法&#xff0c;那就是录音转文字软件。它像个语言魔术师&#xff0c;将我所…

一文告诉你,Intellij IDEA神器隐藏的11种实用小技巧!

el/2022/10/4 6:58:50 点击上方“程序员大咖”&#xff0c;选择“置顶公众号” 关键时刻&#xff0c;第一时间送达&#xff01; Intellij IDEA真是越用越觉得它强大&#xff0c;它总是在我们写代码的时候&#xff0c;不时给我们来个小惊喜。出于对Intellij IDEA的喜爱&#x…

Docker制作SpringBoot镜像

Dcokerfile目录 编写Dockerfile FROM openjdk:8 #发布到网上时只会把jar包和Dockerfile发布上去RUN mkdir -p /opt/javaCOPY app.jar /opt/java/app.jar #地址映射 #CMD ["--server.port8080"] #对外暴露端口(可以任意修改) EXPOSE 15009 #执行命令 #ENTRYPOINT [&q…

安卓读取,添加,更新,删除联系人,读取短信

目录 读取联系人 添加联系人 更新联系人 删除联系人 读取短信 读取联系人 安卓可以通过contentResolver来读取联系人表&#xff0c;联系人表的Uri信息是&#xff1a;content://com.android.contacts/data/phones 从而输出联系人信息&#xff0c; 需要相关权限&#xff1a…

Pod进阶

目录 一、资源限制 1. CPU 资源单位 2.内存 资源单位 3.示例 二、重启策略 三、健康检查&#xff08;探针) 1.探针的三种规则&#xff1a; 1.1就绪探测 2.Probe支持三种检查方法&#xff1a; 2.1exec检查方式 2.2httpGet方式 2.3tcpSocket方式 3. 启动、退出动作…

Sketch打不开AI文件?转换方法在这里

1、对比设计软件 Sketch 与 AI 软件功能 Sketch 与 Illustrator 都是行业内优秀的矢量图形设计软件&#xff0c;各有千秋。Sketch 从 2010 年面世&#xff0c;专注 APP 界面设计&#xff0c;深受初学者与专业人士喜爱。Illustrator 拥有更悠久的历史&#xff0c;是处理复杂图标…

linux系统虚拟主机开启支持SourceGuardian(sg11)加密组件

注意&#xff1a;sg11我司只支持linux系统虚拟主机自主安装。支持php5.3及以上版本。 1、登陆主机控制面板&#xff0c;找到【远程文件下载】这个功能。 2、远程下载文件填写http://download.myhostadmin.net/vps/sg11_for_linux.zip 下载保存的路径填写/others/ 3、点击控制…

前端项目环境变量如何配置?

我们在项目开发过程中&#xff0c;至少会经历开发环境、测试环境和生产环境三个阶段。不同阶段请求的状态&#xff08;如接口地址等&#xff09;不尽相同&#xff0c;若手动切换接口地址是相当繁琐切容易出错的。于是环境变量配置的需求就应运而生&#xff0c;我们只需做简单的…

Window下安装MinGW64

欢迎来到我的酒馆 介绍Windows下&#xff0c;安装MinGW64。 目录 欢迎来到我的酒馆二.MinGW64三.配置系统环境变量 二.MinGW64 从sourceforge下载mingw64&#xff0c; sourceforge下载MinGW https://sourceforge.net/projects/mingw-w64/files/mingw-w64/mingw-w64-release/ 下…

在家查阅下载AACR(美国癌症研究学会)数据库文献

AACR&#xff08;美国癌症研究学会&#xff09;简介&#xff1a; 美国癌症研究学会American Association for Cancer Research创建于1907年&#xff0c;是世界上成立最早、规模最大的致力于全面、创新和高水准癌症研究的科学组织。其出版物包括7种正式出版的期刊&#xff1a; …

基于机器学习的库存需求预测 -- 机器学习项目基础篇(12)

在本文中&#xff0c;我们将尝试实现一个机器学习模型&#xff0c;该模型可以预测在不同商店销售的不同产品的库存量。 导入库和数据集 Python库使我们可以轻松地处理数据&#xff0c;并通过一行代码执行典型和复杂的任务。 Pandas -此库有助于以2D阵列格式加载数据帧&#…

马来西亚的区块链和NFT市场调研

马来西亚的区块链和NFT市场调研 基本介绍 参考&#xff1a; https://zh.wikipedia.org/wiki/%E9%A9%AC%E6%9D%A5%E8%A5%BF%E4%BA%9A zz制度&#xff1a;联邦议会制 语言文字&#xff1a; 马来语 民族&#xff1a; 69.4%原住民&#xff08;土著&#xff09;&#xff0c;23.2%…