LSTM

import torch
from torch import nn

input = torch.randn(5, 3, 10) # (seq_len, batch, input_size)

 input 데이터는 3개, LSTM에 순차적으로 들어갈 시간은 5개, 입력 노드의 개수(설명변수)는 10개라고 하자. torch.randn로 rnn에 들어갈 input을 만들면 첫 번째 원소가 sequence length = 5, 두 번째 원소가 batch size = 3, 세 번째 원소가 input size = 10 인 차원으로 생성한다.

 PyTorch에서 다음과 같이 sequence 길이는 5, 출력 노드의 개수는 20개, LSTM층이 2개인 RNN 구조를 만들어보자.

 

 nn.LSTM에서 입력층의 크기(input_size)는 10 (주황색 화살표), 은닉층의 크기(hidden_size)는 20 (파란색 화살표), 레이어의 개수(num_layers)는 2개인 순환신경망이다. input 데이터를 rnn에 집어 넣으면 output과 (hn, cn)의 튜플을 반환한다.

rnn = nn.LSTM(input_size = 10, hidden_size = 20, num_layers = 2)
output, (hn, cn) = rnn(input)

 

output은 2층 layer의 시간 순서에 따른 출력이다. 첫 번째 원소는 sequence 길이인 5이고, 두 번째 원소는 batch 크기, 세 번째 원소는 20으로 은닉층의 크기(출력층의 크기)이다.

output.size()

> torch.Size([5, 3, 20])

 

 

 

다음과 같이 last sequence의 출력만 사용하려면 output에서 마지막 값을 가져온다. output[-1]의 shape를 확인해보면 batch가 3개, 은닉층(출력)의 크기가 20이 됨을 알 수 있다.

output[-1].size()

> torch.Size([3, 20])

 

 

 다음으로 (hn, cn)은 각각 다음 LSTMcell로 넘겨주는 hidden state, cell sate를 말하고, input으로 (hn, cn)이 주어지지 않으면 c0와 h0는 zeros로 초기화한다. hn과 cn의 shape를 살펴보자.

hn과 cn의 차원의 첫 번째 원소는 2층 레이어이므로 2, 두 번째원 소는 batch 3개, 은닉층의 크기이므로 20이다.

print(hn.size())
print(cn.size())

> torch.Size([2, 3, 20])

> torch.Size([2, 3, 20])

 

 

Bidirectional LSTM

nn.LSTM에서는 bidirectional = True로 설정하므로써 간단하게 위와 같은 biderectional LSTM을 구현할 수 있다.

import torch
from torch import nn

input = torch.randn(5, 3, 10) # (seq_len, batch, input_size)
rnn = nn.LSTM(input_size = 10, hidden_size = 20, num_layers = 2, bidirectional = True)
output, (hn, cn) = rnn(input)

 이 때 output은 왼쪽에서 오른쪽으로 한 개, 오른쪽에서 왼쪽으로 한 개가 concatenate 되어 출력이 된다. 따라서 output의 hidden_size(출력층의 크기)는 두 배인 2 * 20 = 40이 된다.

 

output.size()

> torch.Size([5, 3, 40])

 

 

 hn과 cn의 첫 번째 원소는 bidirection이므로 2개 * 2층 레이어이므로 2개 = 총 4개이다. 다음 LSTM cell로 넘겨주는 hn과 cn은 hidden_size가 concatenate 되지 않으므로 20인 것을 알 수 있다.

print(hn.size())
print(cn.size())

> torch.Size([4, 3, 20])

> torch.Size([4, 3, 20])

CART(Calssification and Regression Trees, Beriman 1984) 알고리즘을 분류목적으로 사용하는 경우에는 불순도 측도로 지니 지수(Gini index)나 엔트로피(Entropy)를 사용한다.

 

지니 지수

엔트로피 지수

 

 

  Sepal.Length (cm) Sepal.Width (cm) Petal.Length (cm) Petal.Width (cm) Species
1 4.6 3.4 1.4 0.3 setosa
2 4.7 3.2 1.6 0.2 setosa
3 5.2 3.5 1.5 0.2 setosa
4 5.0 3.2 1.2 0.2 setosa
5 6.1 3.0 4.6 1.4 versicolor
6 6.1 2.9 4.7 1.4 versicolor
7 5.2 2.7 3.9 1.4 versicolor
8 5.7 2.8 4.1 1.3 versicolor
9 5.8 2.7 3.9 1.2 versicolor
10 6.3 2.9 5.6 1.8 verginica
11 7.7 2.8 6.7 2.0 verginica

 

총 지니지수

연속형 변수를 두 개의 자식마디로 나누는 방법에는 다음과 같은 방법이 있다.

1. 모든 기준점으로 불순도 계산

2. 중위수, 사분위수를 기준으로 불순도 계산

3. 평균을 기준으로 불순도 계산

4. Class가 바뀌는 점을 기준으로 불순도 계산

 

여기서는 간단하게 위의 그림에서 나온대로 자식마디를 나누고 지니지수를 계산해보자.

 

 

'Machine Learning' 카테고리의 다른 글

AdaBoost  (0) 2020.07.01
Random Forest  (0) 2020.06.28

 랜덤 포레스트(Random Forest)는 여러 개의 의사결정나무(Decision Tree)를 이용하여 높은 성능의 예측 모형을 만드는 앙상블 기법의 기계학습 방법이다. 앙상블 방법은 주어진 자료로부터 여러 개의 예측 모형을 구축한 후, 이 여러 모형의 예측 결과들을 종합하여 단순 다수결 투표를 통해 최종적으로 하나의 모형을 만들어 낸다. 이와 같이 여러 개의 의사결정나무를 사용해 만든 랜덤 포레스트는 한 개의 의사결정 나무를 사용하는 경우보다 정확성과 일반화 성능이 높다.

 

 랜덤 포레스트 알고리즘은 훈련 자료(Training set)로부터 B개의 부트스트랩(Bootstrap) 표본을 얻은 후 각각의 표본에 대해 p개의 설명 변수 중 m개를 랜덤하게 선택하여 의사결정 나무를 생성한다. 크게 훈련(training)단계와 검정(test) 단계로 나누어서 살펴보면, 훈련 단계에서는 먼저 중복을 허용하는 표본 추출 기법인 부트스랩 방법을 이용하여 크기가 N인 B개의 부트스트랩 표본을 생성한다. 이때 표본 추출 과정에서 선택되지 않은 표본을 OBB(Out-of-Bag)라고 한다. 그 후 p개의 설명 변수 중 m개를 무작위로 선택하여 의사결정나무를 생성한다. 이러한 과정을 B번 반복하게 되면 B개의 의사결정나무가 생성되며, 검정 단계에서는 훈련 단계에서 사용되지 않은 OBB들에 대하여 각각 B개의 의사결정나무를 적용여 OBB 에러를 측정한다.

 

 최종적으로 랜덤 포레스트는 B개의 다양한 의사결정나무 분류기(classifier)를 갖게 되고 새로운 표본이 입력되면 각 의사결정나무 분류기를 통해 예측된 class 중 가장 많이 득표한 class를 최종 예측으로 분류한다.

 

Random Foest 구조

 

 랜덤 포레스트는 의사결정나무 생성 과정에서 랜덤하게 부트스트랩 표본을 추출하고 랜덤하게 변수들을 선택하는 과정을 반복하기 때문에 만들어지는 의사결정나무의 다양성을 높여준다. 이러한 랜덤 포레스트의 무작위성에 의해 서로 다른 특성을 갖는 의사결정나무들의 최종적인 투표를 통해 결정된 예측은 한 개의 의사결정나무를 사용하는 경우보다 정확성과 일반화 안정성이 더 좋다.

 

Decision Teree 구조

 

 랜덤포레스트 의사결정나무 생성 방법

 부트스트랩 표본을 구성하고 설명변수 p개 중 설명변수 m개를 랜덤하게 선택한다. 이때 선택하는 m은 보통 √p나 p/3을 선택한다. m개의 설명변수 중 가장 지니 지수(Gini index)를 작게하는 설명 변수와 분할 점을 선택하고 두 개의 자식 마디(daughter node)로 분할한다. 가지치기 없이 CART(Classification and Regression Trees, Brieman 1984) 알고리즘을 이용하여 의사결정나무를 생성하거나 또는 뿌리마디(root node)에서 끝마디(terminal node)까지의 깊이가 너무 크면 과적합이 발생할 수 있기 때문에 적절한 최소마디 크기를 정하여 의사결정나무를 생성한다.

 

랜덤포레스트의 의사결정나무 개수에 따른 OBB에러

 

설명변수 개수에 따른 OBB 에러

 

랜덤포레스트 평균지니감소에 따른 변수 중요도

'Machine Learning' 카테고리의 다른 글

AdaBoost  (0) 2020.07.01
지니지수(Gini index)를 이용한 최적 분류점 찾기  (0) 2020.06.28
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch import optim
import tqdm
import copy
from sklearn.metrics import confusion_matrix, classification_report

class Trainer:
    def __init__(self, net, train_loader, test_loader, criterion, epochs=100, lr = 0.001, l2_norm = None, device=None):
        self.net = net
        self.train_loader = train_loader
        self.test_loader = test_loader
        self.criterion = criterion
        self.epochs = epochs
        self.lr = lr
        self.l2_norm = l2_norm

        if device is not None:
            self.device = device
        else:
            self.device = 'cuda:0' if torch.cuda.is_available() else 'cpu'

        self.train_losses = []
        self.train_acc = []
        # self.val_losses = []
        self.val_acc = []

        self.best_model_wts = copy.deepcopy(self.net.state_dict())
        self.best_acc = 0.

    def train_net(self):
        optimizer = optim.Adam(self.net.parameters(), lr = self.lr)
        for epoch in range(self.epochs):
            running_loss = 0.
            self.net.train()
            n = 0
            n_acc = 0

            for i, (X_batch, y_batch) in enumerate(tqdm.tqdm(self.train_loader, total = len(self.train_loader))):
                X_batch = X_batch.to(self.device, dtype=torch.float)
                y_batch = y_batch.to(self.device, dtype=torch.int64)
                y_pred_batch = self.net(X_batch)

                # regularization
                if self.l2_norm is not None:
                    lambda2 = self.l2_norm
                    fc_params = torch.cat([x.view(-1) for x in self.net.out.parameters()])
                    l2_regularization = lambda2 * torch.norm(fc_params, p=2)
                else:
                    l2_regularization = 0.

                loss = self.criterion(y_pred_batch, y_batch) + l2_regularization
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()

                running_loss += loss.item()
                n += len(X_batch)
                _, y_pred_batch = y_pred_batch.max(1)
                n_acc += (y_batch == y_pred_batch).float().sum().item()
            self.train_losses.append(running_loss / i)
            self.train_acc.append(n_acc / n)

            # 검증 데이터의 예측 정확도
            self.val_acc.append(self.eval_net(self.test_loader, self.device))

            # epoch 결과 표시
            print('epoch: {}/{}, train_loss: {:.4f}, train_acc: {:.2f}%, test_acc: {:.2f}%'.format(epoch + 1, self.epochs,
                                                                                                 self.train_losses[-1],
                                                                                                 self.train_acc[-1] * 100,
                                                                                                 self.val_acc[-1] * 100))
        print('best acc : {:.2f}%'.format(self.best_acc * 100))

    def eval_net(self, data_loader, device):
        self.net.eval()
        ys = []
        y_preds = []
        for X_batch, y_batch in data_loader:
            X_batch = X_batch.to(device, dtype=torch.float)
            y_batch = y_batch.to(device, dtype=torch.int64)

            with torch.no_grad():
                _, y_pred_batch = self.net(X_batch).max(1)
            ys.append(y_batch)
            y_preds.append(y_pred_batch)

        ys = torch.cat(ys)
        y_preds = torch.cat(y_preds)

        acc = (ys == y_preds).float().sum() / len(ys)

        if acc.item() > self.best_acc:
            self.best_acc = acc
            self.best_model_wts = copy.deepcopy(self.net.state_dict())

        return acc.item()

    def evaluation(self, data_loader, device):
        model = self.get_best_model()
        ys = []
        y_preds = []
        for X_batch, y_batch in data_loader:
            X_batch = X_batch.to(device, dtype=torch.float)
            y_batch = y_batch.to(device, dtype=torch.int64)

            with torch.no_grad():
                _, y_pred_batch = self.net(X_batch).max(1)
            ys.append(y_batch)
            y_preds.append(y_pred_batch)

        ys = torch.cat(ys)
        y_preds = torch.cat(y_preds)

        acc = (ys == y_preds).float().sum() / len(ys)
		
        print("Confusion Matrix")
        print(confusion_matrix(ys.to('cpu'), y_preds.to('cpu')))
        print("Classification Report")
        print(classification_report(ys.to('cpu'), y_preds.to('cpu'), digits = 4))

        return acc.item()


    def get_best_model(self):
        self.net.load_state_dict(self.best_model_wts)
        return self.net

 

완전연결층 fully connected layer가 포함된 신경망을 준비한다.

import torch
from torch import nn, optim
from torch.nn import functional as F

class net(nn.Module):
    def __init__(self):
        super(net, self).__init__()
        self.conv1_1 = nn.Conv2d(in_channels = 3, out_channels = 32, kernel_size = 7, stride = 3)
        self.bn1_1 = nn.BatchNorm2d(32)
        self.conv_dropout1_1 = nn.Dropout2d(0.1)
        
        self.conv2_1 = nn.Conv2d(in_channels = 32, out_channels = 64, kernel_size = 5, stride = 2)
        self.bn2_1 = nn.BatchNorm2d(64)
        self.pool1 = nn.MaxPool2d(2, 2)
        self.conv_dropout2_1 = nn.Dropout2d(0.1)
        
        self.conv3_1 = nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, stride = 2)
        self.bn3_1 = nn.BatchNorm2d(128)
        self.conv_dropout3_1 = nn.Dropout2d(0.25)
        self.conv3_2 = nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, stride = 1)
        self.bn3_2 = nn.BatchNorm2d(128)
        self.conv_dropout3_2 = nn.Dropout2d(0.25)
        
        self.conv4_1 = nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 2, stride = 1)
        self.bn4_1 = nn.BatchNorm2d(256)
        self.conv_dropout4_1 = nn.Dropout2d(0.3)
        self.conv4_2 = nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 2, stride = 1)
        self.bn4_2 = nn.BatchNorm2d(256)
        self.conv_dropout4_2 = nn.Dropout2d(0.3)
        self.conv4_3 = nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 2, stride = 1)
        self.bn4_3 = nn.BatchNorm2d(512)
        self.pool2 = nn.MaxPool2d(2, 2)
        self.conv_dropout4_3 = nn.Dropout2d(0.3)
        
                     
        self.fc1 = nn.Linear(512*7*7, 1000)
        self.fc_dropout1 = nn.Dropout(0.5)
        self.fc2 = nn.Linear(1000, 1000)
        self.fc_dropout2 = nn.Dropout(0.5)
        self.fc3 = nn.Linear(1000, 256)
        self.fc_dropout3 = nn.Dropout(0.1)
        self.out = nn.Linear(256, 1)

    def forward(self, x):
        x = self.bn1_1(F.relu(self.conv1_1(x)))
        x = self.conv_dropout1_1(x)
        
        x = self.bn2_1(F.relu(self.conv2_1(x)))
        x = self.conv_dropout2_1(self.pool1(x))
        
        x = self.bn3_1(F.leaky_relu(self.conv3_1(x)))
        x = self.conv_dropout3_1(x)
        x = self.bn3_2(F.leaky_relu(self.conv3_2(x)))
        x = self.conv_dropout3_2(x)
        
        x = self.bn4_1(F.leaky_relu(self.conv4_1(x)))
        x = self.conv_dropout4_1(x)        
        x = self.bn4_2(F.leaky_relu(self.conv4_2(x)))
        x = self.conv_dropout4_2(x)
        x = self.bn4_3(F.leaky_relu(self.conv4_3(x)))
        x = self.pool2(x)
        x = self.conv_dropout4_3(x)
        
        x = x.view(-1, 512*7*7)
        
        x = self.fc_dropout1(F.relu(self.fc1(x)))
        x = self.fc_dropout2(F.relu(self.fc2(x)))
        x = self.fc_dropout3(F.relu(self.fc3(x)))
        out = self.out(x)
        
        return out
    
net = net()

 

Θ = {β,W}이 신경망 net의 모든 parameter일 때, β는 마지막 완전연결층(fc3)에서 예측값 out 사이의 가중치(절편 포함), W는 이 layer 전의 모든 weights의 집합이라고 하자. 이때 모든 parameter와 fc3의 weights의 regularization을 추가한 Loss를 최적화한다.

 

CrossEntropyLoss에 각각 l1규제와 l2규제 추가한 Loss

lambda1과 lambda2는 과적합을 방지하기 위한 정규화 모수이다.

lambda1 = 0.01
lambda2 = 0.005

fc_params = torch.cat([x.view(-1) for x in net.out.parameters()])
l1_regularization = lambda1 * torch.norm(fc_params, p=1)
l2_regularization = lambda2 * torch.norm(fc_params, p=2)

print(fc_params.size())

last fully conncected layer에서 prediction layer 사이의 가중치는 256개의 coefficients와 1개의 bias로 총 257개이다

 

 

loss_fn = nn.CrossEntropyLoss()
loss = loss_fn(ypred, y) + l1_regularization # loss에 마지막 fc layer의 parameter l1 규제 추가
optimizer.zero_grad()
loss.backward()
optimizer.step()

 

'PyTorch' 카테고리의 다른 글

[PyTorch] Focal Loss  (0) 2021.04.22
[PyTorch] nn.LSTM input, output shape  (0) 2020.06.30
[PyTorch] Training 헬퍼 함수 만들기  (0) 2020.06.17
[PyTorch] PyTorch 1.5.0 설치하기  (0) 2020.06.16

1. PyTorch 설치를 위해 Anaconda에 새로운 가상환경("pytorch")을 생성하고 활성화시킨다

 

2. PyTorch 홈페이지에서 설치할 버전을 선택하여 설치 명령어를 찾는다. GPU 가속을 사용하기 위해 CUDA 10.2 버전으로 선택한다.

https://pytorch.org/get-started/locally/

 

PyTorch

An open source deep learning platform that provides a seamless path from research prototyping to production deployment.

pytorch.org

conda install pytorch torchvision cudatoolkit=10.2 -c pytorch

 

아래 NVIDIA 홈페이지에서 CUDA도 찾아서 설치한다

https://developer.nvidia.com/cuda-10.2-download-archive

 

CUDA Toolkit 10.2 Download

Select Target Platform Click on the green buttons that describe your target platform. Only supported platforms will be shown. Operating System Architecture Distribution Version Installer Type Do you want to cross-compile? Yes No Select Host Platform Click

developer.nvidia.com

 

3. 활성화된 가상환경에서 conda 명령어를 입력하여 PyTorch 설치

 

4. 주피터 노트북을 실행하고 PyTorch가 설치된 가상환경 커널로 선택하여 빈 노트를 작성한다.

PyToch를 import 한다. cuda가 정상적으로 사용가능한지 확인해본다.

import torch

torch.cuda.is_available()

변수를 생성해서 GPU로 보내본다.

x = torch.randn(5).to("cuda:0")
print(x)

cuda가 정상적으로 사용가능하다

주피터 노트북에서 파이토치 설치가 끝났다

1. 가상환경 생성

Anaconda Prompt에서 다음 명령을 입력하여

conda create -n pytorch python=3.7

"pytorch"라는 이름의 python 3.7 환경(env)를 만든다

 

 

 

2. 생성된 가상환경 리스트 확인

conda info --envs

 

 

3. 가상환경 활성화 시키기

방금 만든 가상활경을 활성화하기 위해서 (conda) activate 가상환경을 입력한다

activate pytorch

가상환경 "pytorch"로 바뀌었다

 

 

4. 주피터 노트북에 가상환경 추가하기

활성화시킨 가상환경에 jupyter notebook을 설치한다

pip install jupyter

설치가 완료되면 ipython kernel install --name "가상환경" --user 을 입력해 주피터 노트북에 가상환경을 추가

ipython kernel install --name "pytorch" --user

주피터 노트북을 실행

jupyter notebook에 가상환경 "pytorch"가 생겼다

 

 

5. 주피터 노트북에서 생성된 가상환경 커널 삭제하기

Anaconda prompt에서 jupyter kernelspec list 를 입력하여 주피터 노트북에 생성된 커널 목록을 확인한다

jupyter kernelspec remove "kernel name" 를 입력하여 불필요한 가상환경 커널을 삭제한다

주피터에서 "tensorflow2" 커널이 삭제되었다

 

 

5. 가상환경 삭제하기

"pytorch"라는 이름의 가상환경을 삭제한다

conda remove -n pytorch --all

 

'기타' 카테고리의 다른 글

주식/비트코인 보조지표들 계산 python  (0) 2021.06.17
비트코인 분봉 데이터 가져오기  (0) 2021.06.17
python openslide 설치법  (0) 2020.06.15

1. openslide python 1.1.1과 Windows Binaries 2017-11-22 64bit를 다운로드하고 C:\에 압축을 푼다.

https://openslide.org/download/

 

Downloading OpenSlide

Downloading OpenSlide OpenSlide and its official language bindings are available under the terms of the GNU Lesser General Public License, version 2.1. Source OpenSlide (stable API) 3.4.1 2015-04-20 tar.gz tar.xz 3.4.0 2014-01-25 tar.gz tar.xz 3.3.3 2013-0

openslide.org

openslide python interface 1.1.1 2016-06-11 tar.gz 다운로드
windows binaries 2017-11-22 64-bit 다운로드
문서에 두 압축파일을 풀어놓았다

 

2. Microsoft Visual Studio 다운로드한다.

https://visualstudio.microsoft.com/ko/downloads/

 

Windows 및 Mac용 Visual Studio 2019 다운로드

Visual Studio Community, Professional 및 Enterprise를 다운로드하세요. 지금 Visual Studio IDE, Code 또는 Mac을 무료로 사용해 보세요.

visualstudio.microsoft.com

커뮤니티 버전 다운로드

 

Visual C++ 빌드 도구 선택, 옵션에서 위에서부터 3개 선택, 설치

 

 

Microsoft Visual C++설치하지 않고 openslide setup.py 실행시 아래와 같은 에러가 발생한다. 

microsoft visual c++ 14.0 is required. get it with "build tools for visual studio":visualstudio.microsoft.com/downloads/

 

 

 

 

 

3. Anaconda Prompt에서 cd ~Documents\openslide-python-1.1.1 를 입력해서 압축푼 주소로 이동하고 python setup.py install 입력하여 setup.py를 실행하여 openslide를 설치한다.

cd C:\openslide-python-1.1.1
python setup.py install

openslide가 설치되었다!

 

 

3. "C:\openslide-win64-20171122\bin" 폴더 주소를 환경변수에 추가

윈도우 검색창에 "고급 시스템 설정" -> 환경변수 -> 시스템 변수에서 Path를 선택하고 편집 클릭

 

4. 주피터 노트북 실행하여 PIL 라이브러리를 설치한다

pip install pillow

설치하지 않고 openslide를 import하면 PIL을 필요하다는 에러가 발생한다

 ModuleNotFoundError: No module named 'PIL'

 

5. 주피터 노트북에서 openslide를 import 하기

환경변수에 windows binaries가 있는 bin 폴더를 추가한다

import os
os.environ['PATH'] = "C:\\openslide-win64-20171122\\bin" + ";" + os.environ['PATH']

현재 working directory를 openslide-python-1.1.1 폴더로 바꾼다

os.chdir('C:/Users/KIMSUNGHUN/Documents/openslide-python-1.1.1')
print(os.getcwd())

openslide를 import 한고 TCGA의 GBM histopathology 이미지 확장자인 .svs 파일을 불러온다

import openslide
path = "C:/data\TCGA_GBM/raw files/TCGA-02-0003-01Z-00-DX1.6171b175-0972-4e84-9997-2f1ce75f4407.svs"
osr = openslide.OpenSlide(path)
print(osr.dimensions)

openslide로 TCGA-GBM histopathology 이미지 파일인 .svs를 불러왔다

 

자세한 OpenSlide의 사용법은 OpenSlide 홈페이지 참고

https://openslide.org/api/python/

 

OpenSlide Python — OpenSlide Python 1.1.1 documentation

OpenSlide Python OpenSlide Python is a Python interface to the OpenSlide library. OpenSlide is a C library that provides a simple interface for reading whole-slide images, also known as virtual slides, which are high-resolution images used in digital patho

openslide.org

 

+ Recent posts