
-
BNNS Graph의 새로운 기능
이제 개발자는 BNNS Graph Builder API를 통해 익숙한 Swift 언어를 사용하여 작업 그래프를 작성함으로써 사전 및 사후 처리 루틴 및 소규모 머신 러닝 모델을 생성할 수 있습니다. BNNS는 실행하기 전에 그래프를 컴파일하고 오디오 처리와 같이 실시간 및 지연에 민감한 사용 사례를 지원합니다. 이 세션에서는 작년 비트 크러셔 예제를 다시 검토하고, 별도의 Python 파일에 의존하지 않고 Swift 구성 요소를 단순화하며, 대신 Swift의 오디오 효과를 온전히 구현합니다. BNNS Graph Builder API는 또한 이미지 데이터를 머신 러닝 모델에 전달하기 전에 사전 처리하는 데에도 적합합니다. 이 세션에는 알파 채널로 이미지에서 투명한 픽셀을 클리핑하는 방법을 보여주는 데모도 포함되어 있습니다.
챕터
- 0:00 - 서론
- 3:12 - BNNSGraph 요약
- 6:15 - BNNSGraphBuilder
- 16:58 - BNNSGraphBuilder 사용하기
리소스
관련 비디오
WWDC25
WWDC24
-
비디오 검색…
안녕하세요 저는 Simon Gladman이고 Apple의 Vector & Numerics Group에서 일하고 있습니다 저희 그룹은 CPU로 고성능 및 에너지 효율적인 계산을 위한 각종 라이브러리 세트를 제공합니다 이 라이브러리는 이미지 및 신호 처리 선형 대수 그리고 오늘 제가 설명할 머신 러닝을 지원합니다 Basic Neural Network Subroutines 또는 BNNS는 머신 러닝 라이브러리입니다 이것을 사용하면 앱에 CPU 기반의 추론 기능을 추가할 수 있으며 오디오 처리 같은 실시간 및 저지연 환경에 안성맞춤입니다 오디오에서 보컬을 분리 또는 제거하기 콘텐츠에 따라 오디오를 다양한 영역으로 분할하기 한 악기의 소리를 다른 악기의 소리로 변환하는
톤 전송 등의 기능을 구현할 수 있습니다
BNNS는 이미지 같은 다른 데이터와도 완벽하게 호환됩니다 따라서 앱에 고성능 추론이 필요하다면 BNNS가 좋습니다
작년에 선보인 새로운 API인 BNNSGraph는 BNNS의 속도와 에너지 효율성을 더 높여 주며 사용이 더 쉽습니다 BNNSGraph를 시연하며 Bitcrusher 오디오 유닛을 만들어 Logic Pro와 Garageband에
자신만의 이펙트를 추가하는 것이 얼마나 쉬운지 보여드렸습니다
올해에는 Swift의 성능을 바탕으로 추론용 소형 모델과 전처리 및 후처리용 연산 그래프를 생성할 수 있는 향상된 프로그래밍 인터페이스를 BNNSGraph에 제공합니다 오늘 설명할 것은 BNNSGraph의 새 기능인 BNNSGraphBuilder입니다 오늘 세션은 먼저 BNNSGraph를 간단히 복습하겠습니다 BNNSGraph가 무엇이며 머신 러닝 및 AI 모델 최적화와 적용 방법을 설명하겠습니다 그런 다음 BNNSGraphBuilder를 소개합니다 이 기능을 정말 쉽게 구현하는 워크플로를 요약하고 소개의 일환으로 간단한 그래프 작성 방법을 시연하겠습니다 BNNSGraphBuilder 소개 후 세 가지 데모를 진행하겠습니다 먼저 Graph Builder로 이미지를 전처리합니다 그리고 ML 모델이 생성한 데이터를 새로운 API로 후처리합니다 마지막으로 작년의 Bitcrusher 데모를 업데이트하여 Swift로 연산 그래프를 빌드합니다 그럼 바로 시작하겠습니다 BNNSGraph는 오디오와 기타 지연이 중요한 사례에 권장됩니다 메모리 할당과 멀티스레딩을 제어할 수 있으니까요 두 경우 모두 커널 코드로의 컨텍스트 스위치를 유발 가능하며 실시간 데드라인을 못 지킬 수 있습니다 지난해까지는 컨볼루션 행렬곱 또는 활성화와 같은 개별 레이어를 사용해 BNNS 기반 네트워크를 구축했을 겁니다 BNNS로 기존 모델을 구현하려면 각 레이어를 BNNS 프리미티브로 코딩하고 모든 중간 텐서에 코드를 작성해야 했습니다 BNNSGraph는 여러 레이어 및 레이어 간 데이터 흐름으로 구성된 전체 그래프를 가지고 단일 그래프 객체로 사용합니다
즉 개발자가 개별 레이어 코드를 작성하지 않아도 되죠 게다가 개발자와 사용자는 더 우수한 속도 및 에너지 효율의 이점을 얻습니다
BNNSGraph는 전체 모델을 인식하기 때문에 이전에는 불가능했던 여러 가지 최적화를 수행할 수 있습니다 이 최적화는 실행 시간, 메모리 사용량, 에너지 비용을 줄여줍니다 게다가 비용도 없죠 이 모델의 일부 섹션을 사용하여 이 최적화 일부를 살펴보겠습니다 최적화 중 하나는 수학적 변환입니다 예를 들어 슬라이스 연산을 재배치하여 BNNS가 해당 슬라이스의 요소 하위 집합만 계산하도록 합니다 또 다른 최적화는 레이어 융합입니다 예를 들어 컨볼루션 레이어와 활성화 레이어를 단일 연산으로 융합합니다 그리고 복사 생략은 슬라이스의 데이터를 복사하는 대신 해당 데이터 하위 집합을 참고하여 복사를 피합니다 또한 BNNSGraph는 텐서의 메모리를 공유해 메모리 사용을 최적화하고 불필요한 할당을 제거합니다 마지막으로 BNNSGraph의 가중치 재조정 최적화는 가중치를 재조정하여 캐시 로컬리티를 개선합니다 코드를 전혀 작성하지 않아도 이러한 최적화가 가능합니다 바로 이렇게요 지난해 소개한 파일 기반 API로 그래프를 생성하려면 CoreML 패키지부터 시작합니다 Xcode는 패키지를 mlmodelc 파일로 자동 컴파일하고 이 mlmodelc 파일에서 그래프 생성 코드를 작성합니다 마지막으로 컨텍스트를 생성하여 그래프를 래핑하면 해당 컨텍스트가 추론을 수행합니다 이 방식은 기존 PyTorch 모델을 프로젝트에 통합하는 가장 좋은 방법입니다 그러나 소형 모델이나 연산 그래프의 경우 더 직관적인 워크플로가 필요할 수 있습니다 Swift로 그래프의 개별 요소를 정의할 수 있다면 어떨까요? 올해에는 바로 그 기능을 제공하는 새로운 API인 BNNSGraphBuilder를 소개합니다
새로운 BNNSGraphBuilder API는 개발자가 익숙한 Swift 언어로 연산 그래프를 작성할 수 있도록 합니다 Swift로 전처리 및 후처리 루틴이나 소형 머신 러닝 모델을 직접 작성할 수 있습니다 Swift 코드에서 단일 함수 호출로 컨텍스트를 생성하며 중간 단계가 없습니다 Swift로 그래프를 작성하고 다른 코드를 인라인으로 확인하면 즉시 이점을 얻을 수 있습니다 익숙한 언어를 사용하며 Swift 프로젝트가 컴파일될 때 그래프가 유형 검사를 받습니다 그래프를 생성하기 전에 런타임 시점에 알려진 값을 Swift와 그래프 간에 공유할 수 있습니다 예를 들어 그래프 초기화 전에 런타임 때 텐서의 형상을 알고 있다면 형상을 그래프 코드에 직접 전달하여 유연한 크기 대신 정적 크기의 나은 성능을 활용할 수 있습니다 BNNSGraphBuilder로 중간 텐서의 형상이나 데이터 유형 같은 속성을 쿼리할 수도 있으며 이는 그래프 디버깅에 도움이 됩니다 유형 텐서는 Xcode의 자동 완성을 활성화하고 런타임 오류 발생 가능성을 줄여줍니다 새로운 구문, 유형 확인 새로운 메서드 및 연산자 중간 텐서 쿼리 방법 등 이전에는 없던 기능을 모두 살펴보겠습니다 올해 BNNSGraph 객체에 추가된 새 유형 메서드는 makeContext입니다 이 새 메서드는 Swift 코드를 재사용 가능 컨텍스트로 변환하여 그래프를 실행하는 데 사용됩니다 컨텍스트는 일반적으로 앱이 시작될 때 한 번만 생성합니다 앱은 필요할 때 컨텍스트를 실행하며 그래프를 전체적으로 처리하는 최적화를 활용합니다 이제 makeContext를 사용하는 라이브 코딩을 살펴보겠습니다 이 메서드는 클로저를 허용하며 이 클로저를 사용하여 그래프를 구성하는 일련의 연산을 정의합니다
그래프는 일반적으로 하나 이상의 인수를 허용합니다 여기서는 클로저의 builder 매개변수로 x와 y라는 두 인수를 지정합니다
매개변수는 모두 부동 값으로 구성된 8개 요소 벡터입니다 이 예시에서 그래프는 두 입력 인수에 대한 요소별 곱셈을 수행하고
결과를 product라고 명명한 출력 인수에 작성합니다 제 그래프는 또한 product의 평균 값을 계산하고 그 값을 mean이라는 두 번째 출력 인수에 작성하며 마지막으로 두 출력 인수를 반환합니다
디버깅을 돕기 위해 BNNSGraphBuilder가 생성하는 중간 및 출력 텐서의 세부 정보를 출력할 수 있습니다
이 예시에서는 mean의 형상을 출력하여 형상이 1인지, 다시 말해 하나의 요소만 포함하는지 확인했습니다 컨텍스트가 생성되면 이를 쿼리하여 인수와 그 위치를 도출할 수 있습니다 여기서 코드는 컨텍스트의 인수 이름을 쿼리하고 그래프의 출력과 입력을 나타내는 텐서 배열을 생성합니다 BNNSGraph는 인수 이름을 출력이 먼저 오고 입력이 뒤에 오도록 정렬합니다
BNNSGraphBuilder API가 제공하는 풍부한 함수 집합을 다양한 데이터 소스에서 인수를 초기화하거나 데이터를 복사 또는 참고하는 데 사용할 수 있습니다 이 예시에서는 코드가 Swift 배열에서 입력을 초기화합니다
입력 및 출력 인수가 할당되면 execute 함수를 호출하여 그래프를 실행하고 두 출력에 결과를 채웁니다
함수를 실행하여 결과를 출력할 수 있습니다
단순한 곱셈과 평균 계산이 전부가 아닙니다 BNNSGraphBuilder를 간단히 둘러보고 새로운 API가 제공하는 다른 연산을 살펴보겠습니다 BNNSGraphBuilder에 포함된 기본 기능의 작은 예시입니다 예를 들어 행렬곱과 컨볼루션, 축소 연산 gather 및 scatter 연산 padding, reshape transpose 같은 연산이 있습니다 새로운 API는 많은 연산을 단순한 Swift 연산자로 지원합니다 산술 연산자로는 곱셈, 덧셈 뺄셈, 나눗셈이 있습니다 요소별 비교 연산자로는 같음 작음, 큼이 있습니다 그리고 세트를 완성하기 위한 요소별 논리 연산자가 있습니다 API 자체에 대해 간단히 소개했습니다 이제 Swift 개발자라면 꽤 익숙할 GraphBuilder API의 두 기능을 자세히 살펴보겠습니다 먼저 강한 형식 검사입니다 강한 형식 검사는 오류를 런타임 시점이 아닌 컴파일 시점에 잡아낼 수 있습니다 BNNSGraphBuilder API는 주어진 연산에 대한 텐서의 데이터 유형이 올바른지 확인합니다 작동 방식을 살펴보겠습니다 이 예시에서는 그래프가 부동 소수점 기본 값을 정수 지수로 승산한 요소별 결과를 반환합니다
이 계산은 정수 지수를 FP16으로 캐스팅하여 수행됩니다 캐스팅 없이 이 계산을 시도하면 makeContext 메서드가 컴파일되지 않습니다 그래프는 mask0의 값이 mask1의 대응값보다 작은 요소를 0으로 만들어 결과를 마스크합니다 비교로 생성된 텐서 요소가 부울이므로 그래프는 추가 캐스팅을 수행하여 조건과 결과를 곱합니다 즉 캐스트 연산 없이 makeContext 메서드가 컴파일되지 않습니다 이 유형의 오류는 컴파일 시점에 발견하여 앱이 사용자에게 제공되기 전에 해결하는 것이 가장 좋습니다 이것이 강한 형식 검사입니다 이제 BNNSGraphBuilder가 텐서를 사용한 부분 선택 즉 슬라이싱하는 방법을 살펴보겠습니다 이 또한 Swift 개발자라면 매우 익숙할 겁니다
슬라이스는 텐서의 특정 부분을 보여주는 창과 같습니다 예를 들어 슬라이스는 행렬의 단일 열이나 행일 수 있습니다 BNNSGraph의 장점은 슬라이스를 기존 데이터에 대한 참고로 처리한다는 것입니다 데이터 복사나 추가 메모리 할당 같은 오버헤드 없이 말이죠 텐서 슬라이싱은 일반적인 연산입니다 새로운 GraphBuilder API로 텐서의 슬라이스를 소스 또는 대상 연산으로 지정할 수 있습니다 이미지를 머신 러닝 모델에 전달하기 전에 관심 영역으로 자르고 싶다고 가정해 보겠습니다 다람쥐가 점심을 즐기는 이 사진의 가운데에서 GraphBuilder API로 정사각형 영역을 선택하는 것이 얼마나 쉬운지 살펴보겠습니다 두 vImage 픽셀 버퍼를 정의합니다 픽셀 버퍼는 이미지 데이터 크기, 비트 깊이 및 채널 수를 저장합니다 첫째 버퍼인 source에는 다람쥐 사진이 포함됩니다 둘째 픽셀 버퍼인 Destination에는 정사각형 자르기가 포함됩니다 각 픽셀 버퍼는 빨강, 녹색 파랑의 3개 채널로 구성되며 각 채널은 32비트 부동 소수점 형식입니다 vImage 픽셀 버퍼의 자세한 내용은 vImage 문서에서 확인해 보세요 수평 및 수직 여백은 640x640 자르기가 소스 이미지의 중심에 위치하도록 합니다 다음은 슬라이스 연산으로 자르기를 수행하는 그래프입니다 먼저 소스를 인수로 정의하고 높이와 너비를 유연하게 지정합니다 이 경우에 음수 값 -1을 전달하면 BNNSGraph에 해당 크기가 어떤 값이라도 될 수 있음을 알립니다 shape 3의 마지막 값은 이미지가 빨강, 녹색, 파랑의 3개 채널을 포함한다고 지정합니다 BNNSGraphBuilder API는 Swift 서브스크립트로 슬라이스를 연산합니다 이 예시에서는 새로운 SliceRange 구조를 사용합니다 수직 및 수평 크기의 시작 인덱스는 해당 여백 값입니다 끝 인덱스를 음수 여백 값으로 설정하면 해당 크기의 끝 인덱스가 마진을 뺀 끝 값이 됩니다
이 예시에서는 채널 크기를 따라 자르지 않으며 코드는 fillAll을 지정하여 세 채널을 모두 포함하도록 합니다
올해에는 vImage 픽셀 버퍼를 위한 새로운 메서드도 도입되었습니다 withBNNSTensor 메서드로 크기 채널 수 같은 속성과 메모리를 픽셀 버퍼와 공유하는 임시 BNNSTensor를 생성할 수 있습니다 이 코드에서 보이듯이 새로운 메서드는 그래프와 이미지 간에 이미지를 매우 간편하게 전달합니다 메모리가 공유되므로 복사나 할당이 필요 없으며 이미지 처리 시 우수한 성능을 발휘합니다 execute 함수 메서드가 반환된 후 대상 픽셀 버퍼에는 잘린 이미지가 포함됩니다 결과를 Core Graphics 이미지로 생성하여 다람쥐가 안전하게 잘렸는지 확인할 수 있습니다 새로운 GraphBuilder 슬라이스 범위 구조 외에도 텐서 슬라이싱 API는 모든 Swift 범위 유형을 지원합니다 따라서 어떤 슬라이싱 요구사항도 지원합니다 슬라이싱에 대한 간략한 개요였습니다 BNNSGraphBuilder와 일부 기능을 살펴봤으니 이제 실제 사용 사례를 보겠습니다 BNNSGraphBuilder API는 ML 모델과 주고받는 데이터의 전처리 및 후처리를 위한 작업 그래프를 구성할 때 매우 알맞습니다 전처리 기술 중 하나는 이미지 임계처리입니다 이는 연속 톤 이미지를 검은색 또는 흰색 픽셀만 포함하는 이진 이미지로 변환하는 것입니다 GraphBuilder로 이를 구현하는 방법을 살펴보겠습니다
그래프를 작성하기 전에 다시 vImage 픽셀 버퍼를 정의합니다 첫 번째는 소스 이미지를 저장하고 두 번째는 임계처리된 이미지를 받는 대상입니다 이 두 픽셀 버퍼는 모두 단일 채널 16비트 부동 소수점 형식입니다 그래프의 첫 번째 단계는 입력 이미지를 나타내는 소스를 정의하는 것입니다 코드에서 이미지 크기에 음수 값을 전달하여 그래프가 모든 이미지 크기를 지원하도록 지정하지만 픽셀 버퍼와 일치하는 FP16 데이터 유형을 명시적으로 지정합니다 연속된 그레이스케일 픽셀의 평균 값을 계산하는 것은 단순히 mean 메서드를 호출하는 것입니다 요소별 '보다 큼' 연산자는 평균 픽셀 값보다 큰 픽셀에 임계처리된 텐서에 1을 할당하고 그렇지 않으면 0을 할당합니다 마지막으로 그래프는 부울 1과 0을 대상 픽셀 버퍼의 비트 깊이로 캐스팅합니다 다시 withBNNSTensor 메서드로 두 이미지 버퍼를 컨텍스트에 전달 후 함수를 실행해 임계처리된 이미지를 생성합니다
날고 있는 펠리컨 한 쌍의 연속적인 회색조 이미지를 그래프에 전달하면 모든 픽셀이 검은색 또는 흰색인 임계처리된 이미지를 얻습니다 BNNSGraphBuilder API의 또 다른 용도는 머신 러닝 모델 결과의 후처리입니다
예를 들어 머신 러닝 모델 결과에 softmax 함수를 적용하고 topK 연산을 수행한다고 가정하겠습니다 여기서 후처리 함수는 작은 그래프를 실시간으로 생성합니다 그래프는 함수의 입력 텐서에 기반한 입력 인수를 선언합니다 그런 다음 입력 인수에 softmax 연산을 적용하고 topK 값과 인덱스를 계산합니다 코드가 topK 함수에 전달하는 k매개변수는 makeContext 클로저의 외부에 정의됩니다 마지막으로 그래프가 topK 결과를 반환합니다 그래프 정의 후 함수는 결과 저장용 텐서를 선언하고 출력 및 입력 텐서를 컨텍스트에 전달합니다 마지막으로 makeArray 메서드는 topK 값과 인덱스를 Swift 배열로 변환하고 반환합니다 다음은 GraphBuilder API를 사용한 데이터 전처리 및 후처리 예시입니다 이제 작년의 Bitcrusher 데모를 살펴보고 Swift 부분을 BNNSGraphBuilder를 사용하도록 업데이트하겠습니다 이 코드처럼 BNNSGraph는 ML 기반 오디오 효과를 손쉽게 추가합니다
작년에는 BNNSGraph를 Audio Unit 확장 앱에 통합하여 시연했습니다 이 앱은 오디오 신호에 실시간 Bitcrusher 효과를 추가했으며 시연에서는 Swift를 사용하여 동일한 효과를 사인파에 적용해 사용자 인터페이스에 효과를 시각으로 표현했습니다 예를 들어 이 사인파가 주어지면 그래프는 해상도를 낮춰 소리에 왜곡을 추가할 수 있으며 시각적 표현은 사인파를 연속적 파형이 아닌 일련의 개별 단계로 보여줍니다 또한 사운드에 풍부함을 더하기 위해 포화 게인을 포함했습니다 지난해 데모는 PyTorch 코드 기반의 CoreML 패키지를 사용했습니다 지난해의 코드를 새 GraphBuilder API를 사용하여 Swift로 작성한 동일한 기능과 비교해 보겠습니다 왼쪽은 PyTorch 코드이고 오른쪽은 새로운 Swift 코드입니다 가장 큰 차이점은 Swift가 중간 텐서를 정의할 때 let과 var를 사용하는 것이므로 이 텐서가 불변인지 가변인지 자유롭게 결정할 수 있습니다 사용 가능한 모든 연산에 접근하기 위한 추가 가져오기가 필요 없습니다 또한 이 경우 tanh와 같은 연산은 텐서의 메서드이며 자유 함수가 아닙니다
요소별 산술 연산자는 두 접근 방식에서 동일합니다
또 다른 차이점은 makeContext 클로저가 둘 이상의 출력 텐서를 반환할 수 있으므로 항상 배열을 반환해야 합니다 Swift GraphBuilder는 makeContext 클로저 내부에 입력 인수를 정의할 수 있습니다 기존 PyTorch 모델이 있는 경우 해당 코드와 기존 파일 기반 API를 사용하는 것이 좋습니다 그러나 신규 프로젝트라면 새 GraphBuilder API를 사용하여 Swift로 그래프를 작성할 수 있으며 이 비교에서처럼 PyTorch로 작성한 동일한 연산 집합과 구조가 유사합니다 또한 BNNSGraph의 16비트 지원을 보여드리겠으며 FP32 연산보다 훨씬 빠르다는 것을 이 예시로 확인할 수 있습니다 다시 Swift 코드입니다 이번에는 정밀도 지정을 위해 유형 별칭을 사용했습니다
유형 별칭을 변경하면 그래프가 FP32 대신 FP16을 사용하게 됩니다 이 예시에서 FP16은 FP32에 비해 훨씬 빠릅니다 요약하자면 BNNSGraphBuilder가 제공하는 Swift API는 사용이 간편하며 고성능 및 에너지 효율적인 모델과 연산 그래프를 작성할 수 있습니다 실시간 및 지연 시간에 민감한 사용 사례에 적합하지만 프로그래밍 인터페이스가 사용자 친화적이므로 다양한 응용 분야에 적용 가능합니다 마지막으로 문서 페이지에서 BNNSGraph 및 BNNSGraphBuilder에 관한 방대한 참고 자료를 확인해 보시기 바랍니다 작년 비디오에서 파일 기반 API 그리고 C에서 BNNSGraph 사용하기에 대해서도 확인해 보세요
시청해 주셔서 감사드리며 좋은 하루 되세요
-
-
8:31 - Introduction to BNNSGraphBuilder
import Accelerate func demo() throws { let context = try BNNSGraph.makeContext { builder in let x = builder.argument(name: "x", dataType: Float.self, shape: [8]) let y = builder.argument(name: "y", dataType: Float.self, shape: [8]) let product = x * y let mean = product.mean(axes: [0], keepDimensions: true) // Prints "shape: [1] | stride: [1]". print("mean", mean) return [ product, mean] } var args = context.argumentNames().map { name in return context.tensor(argument: name, fillKnownDynamicShapes: false)! } // Output arguments args[0].allocate(as: Float.self, count: 8) args[1].allocate(as: Float.self, count: 1) // Input arguments args[2].allocate( initializingFrom: [1, 2, 3, 4, 5, 6, 7, 8] as [Float]) args[3].allocate( initializingFrom: [8, 7, 6, 5, 4, 3, 2, 1] as [Float]) try context.executeFunction(arguments: &args) // [8.0, 14.0, 18.0, 20.0, 20.0, 18.0, 14.0, 8.0] print(args[0].makeArray(of: Float.self)) // [15.0] print(args[1].makeArray(of: Float.self)) args.forEach { $0.deallocate() } }
-
12:04 - Strong typing
// Performs `result = mask0 .< mask1 ? bases.pow(exponents) : 0 let context = try BNNSGraph.makeContext { builder in let mask0 = builder.argument(dataType: Float16.self, shape: [-1]) let mask1 = builder.argument(dataType: Float16.self, shape: [-1]) let bases = builder.argument(dataType: Float16.self, shape: [-1]) let exponents = builder.argument(dataType: Int32.self, shape: [-1]) // `mask` contains Boolean values. let mask = mask0 .< mask1 // Cast integer exponents to FP16. var result = bases.pow(y: exponents.cast(to: Float16.self)) result = result * mask.cast(to: Float16.self) return [result] }
-
14:15 - Slicing
let srcImage = #imageLiteral(resourceName: "squirrel.jpeg").cgImage( forProposedRect: nil, context: nil, hints: nil)! var cgImageFormat = vImage_CGImageFormat( bitsPerComponent: 32, bitsPerPixel: 32 * 3, colorSpace: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(alpha: .none, component: .float, byteOrder: .order32Host))! let source = try vImage.PixelBuffer(cgImage: srcImage, cgImageFormat: &cgImageFormat, pixelFormat: vImage.InterleavedFx3.self) let cropSize = 640 let horizontalMargin = (source.width - cropSize) / 2 let verticalMargin = (source.height - cropSize) / 2 let destination = vImage.PixelBuffer(size: .init(width: cropSize, height: cropSize), pixelFormat: vImage.InterleavedFx3.self) let context = try BNNSGraph.makeContext { builder in let src = builder.argument(name: "source", dataType: Float.self, shape: [ -1, -1, 3]) let result = src [ BNNSGraph.Builder.SliceRange(startIndex: verticalMargin, endIndex: -verticalMargin), BNNSGraph.Builder.SliceRange(startIndex: horizontalMargin, endIndex: -horizontalMargin), BNNSGraph.Builder.SliceRange.fillAll ] return [result] } source.withBNNSTensor { src in destination.withBNNSTensor { dst in var args = [dst, src] print(src) print(dst) try! context.executeFunction(arguments: &args) } } let result = destination.makeCGImage(cgImageFormat: cgImageFormat)
-
17:31 - Preprocessing by thresholding on mean
let srcImage = #imageLiteral(resourceName: "birds.jpeg").cgImage( forProposedRect: nil, context: nil, hints: nil)! var cgImageFormat = vImage_CGImageFormat( bitsPerComponent: 16, bitsPerPixel: 16, colorSpace: CGColorSpaceCreateDeviceGray(), bitmapInfo: CGBitmapInfo(rawValue: CGBitmapInfo.byteOrder16Little.rawValue | CGBitmapInfo.floatComponents.rawValue | CGImageAlphaInfo.none.rawValue))! let source = try! vImage.PixelBuffer<vImage.Planar16F>(cgImage: srcImage, cgImageFormat: &cgImageFormat) let destination = vImage.PixelBuffer<vImage.Planar16F>(size: source.size) let context = try BNNSGraph.makeContext { builder in let src = builder.argument(name: "source", dataType: Float16.self, shape: [-1, -1, 1]) let mean = src.mean(axes: [0, 1], keepDimensions: false) let thresholded = src .> mean let result = thresholded.cast(to: Float16.self) return [result] } source.withBNNSTensor { src in destination.withBNNSTensor { dst in var args = [dst, src] try! context.executeFunction(arguments: &args) } } let result = destination.makeCGImage(cgImageFormat: cgImageFormat)
-
19:04 - Postprocessing
func postProcess(result: BNNSTensor, k: Int) throws -> ([Float32], [Int32]) { let context = try BNNSGraph.makeContext { builder in let x = builder.argument(dataType: Float32.self, shape: [-1]) let softmax = x.softmax(axis: 1) let topk = softmax.topK(k, axis: 1, findLargest: true) return [topk.values, topk.indices] } let indices = context.allocateTensor(argument: context.argumentNames()[0], fillKnownDynamicShapes: false)! let values = context.allocateTensor(argument: context.argumentNames()[1], fillKnownDynamicShapes: false)! var arguments = [values, indices, result] try context.executeFunction(arguments: &arguments) return (values.makeArray(of: Float32.self), indices.makeArray(of: Int32.self)) }
-
21:03 - Bitcrusher in PyTorch
import coremltools as ct from coremltools.converters.mil import Builder as mb from coremltools.converters.mil.mil import ( get_new_symbol ) import torch import torch.nn as nn import torch.nn.functional as F class BitcrusherModel(nn.Module): def __init__(self): super(BitcrusherModel, self).__init__() def forward(self, source, resolution, saturationGain, dryWet): # saturation destination = source * saturationGain destination = F.tanh(destination) # quantization destination = destination * resolution destination = torch.round(destination) destination = destination / resolution # mix destination = destination * dryWet destination = 1.0 - dryWet source = source * dryWet destination = destination + source return destination
-
21:03 - Bitcrusher in Swift
typealias BITCRUSHER_PRECISION = Float16 let context = try! BNNSGraph.makeContext { builder in var source = builder.argument(name: "source", dataType: BITCRUSHER_PRECISION.self, shape: [sampleCount, 1, 1]) let resolution = builder.argument(name: "resolution", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) let saturationGain = builder.argument(name: "saturationGain", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) var dryWet = builder.argument(name: "dryWet", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) // saturation var destination = source * saturationGain destination = destination.tanh() // quantization destination = destination * resolution destination = destination.round() destination = destination / resolution // mix destination = destination * dryWet dryWet = BITCRUSHER_PRECISION(1) - dryWet source = source * dryWet destination = destination + source return [destination] }
-
22:34 - Changing precision
typealias BITCRUSHER_PRECISION = Float16 let context = try! BNNSGraph.makeContext { builder in var source = builder.argument(name: "source", dataType: BITCRUSHER_PRECISION.self, shape: [sampleCount, 1, 1]) let resolution = builder.argument(name: "resolution", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) let saturationGain = builder.argument(name: "saturationGain", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) var dryWet = builder.argument(name: "dryWet", dataType: BITCRUSHER_PRECISION.self, shape: [1, 1, 1]) // saturation var destination = source * saturationGain destination = destination.tanh() // quantization destination = destination * resolution destination = destination.round() destination = destination / resolution // mix destination = destination * dryWet dryWet = BITCRUSHER_PRECISION(1) - dryWet source = source * dryWet destination = destination + source return [destination] }
-
-
- 0:00 - 서론
Apple의 Vector & Numerics Group에서 앱에 CPU 기반의 추론 기능을 추가하는 머신 러닝 라이브러리인 BNNS를 개발했습니다. 이는 특히 실시간 오디오 및 이미지 처리에 유용합니다. 작년에 출시된 BNNSGraph는 속도와 효율성을 개선하고 사용이 쉬웠습니다. 이제 BNNSGraphBuilder가 추가되어 Swift의 성능을 기반으로 소형 모델을 만들고 전처리 및 후처리용 그래프를 생성할 수 있습니다. 세션에서는 이미지 전처리, 데이터 후처리, Bitcrusher 오디오 유닛 샘플 업데이트를 통해 BNNSGraphBuilder의 활용 방법을 시연합니다.
- 3:12 - BNNSGraph 요약
BNNSGraph를 사용하면 메모리 할당과 멀티스레딩을 제어할 수 있으므로 실시간 성능이 개선됩니다. 그러므로 오디오 처리, 실시간 및 저지연 환경에서 작업할 때 유용합니다. 기존의 BNNS에서는 각 레이어를 직접 코딩해야 했지만, BNNSGraph는 전체 그래프를 객체로 활용합니다. 또한 수학적 변환, 레이어 융합, 텐서 메모리 공유를 결합하여 성능과 에너지 효율을 최적화합니다. BNNSGraph는 두 가지 주요 작업 흐름을 제공합니다. CoreML 패키지와 Xcode 컴파일을 사용하는 작업 흐름이나 BNNSGraphBuilder로 소형 모델을 위해 Swift에서 직접 그래프를 정의하는 작업 흐름을 사용할 수 있습니다.
- 6:15 - BNNSGraphBuilder
새 API인 BNNSGraphBuilder를 사용하여 개발자는 Swift에서 직접 연산 그래프를 구성할 수 있습니다. 이렇게 하면 Swift 코드에서 전처리 및 후처리 루틴과 소형 머신 러닝 모델을 만들 수 있습니다. BNNSGraphBuilder의 장점으로는 익숙한 언어를 사용할 수 있고, 컴파일 중에 유형을 검사할 수 있으며, Swift와 그래프 간에 런타임 값을 공유하여 성능을 개선할 수 있다는 점이 있습니다. 이 API는 또한 형상 및 데이터 유형과 같은 속성을 위해 중간 텐서를 쿼리하는 것과 같은 디버깅 기능을 제공하며 Xcode 자동 완성을 활성화합니다. 새 유형 메서드인 ‘makeContext’는 Swift 코드를 그래프 실행을 위해 재사용 가능한 컨텍스트로 변환합니다. 이 컨텍스트는 앱 시작 시 한 번 생성되며, 그 후 여러 번 실행시켜 그래프를 전체적으로 최적화할 수 있습니다. BNNSGraphBuilder API는 광범위한 수학 및 논리 연산을 제공하며, 행렬곱, 컨볼루션, 축소 연산과 같은 일반적인 신경망 기본 기능을 지원합니다. Swift의 BNNSGraphBuilder API는 컴파일 시점에 강한 형식 검사를 통해 오류를 감지하여 텐서 연산에서 올바른 데이터 유형이 사용되도록 합니다. 세션에서는 이 기능을 FP16으로 정수를 캐스팅하는 것과 같은 데이터 유형의 자동 캐스팅으로 시연합니다. 이 기능을 사용하면 컴파일 오류를 줄이고 앱 안정성을 개선할 수 있습니다. 또한 BNNSGraphBuilder API는 텐서 슬라이스를 기존 데이터에 대한 참조로 취급하여 메모리 사용을 최적화합니다. 텐서 슬라이싱은 Swift 서브스크립트와 새로운 SliceRange 구조를 사용하여 실행되므로 Swift 개발자에게 직관적입니다. 이 기능으로 이미지 자르기와 같은 작업을 효율적으로 처리할 수 있습니다. 예시로 vImage 픽셀 버퍼와 ‘withBNNSTensor’ 메서드를 사용하여 다람쥐 사진에서 정사각형의 영역을 잘라내는 과정을 보여 줍니다. 이 과정에서 메모리가 공유되므로 성능이 향상됩니다.
- 16:58 - BNNSGraphBuilder 사용하기
BNNSGraphBuilder는 Swift에서 제공되는 강력한 API로, 오디오 및 이미지를 활용하는 머신 러닝 애플리케이션에서 효율적인 데이터 전처리 및 후처리를 위해 연산 그래프를 구성할 때 유용합니다. 전처리에서는 이 API로 이미지에 임계값을 지정하여 연속적인 색조의 이미지를 이진 이미지로 변환할 수 있습니다. 이 API는 ML 모델 결과에 softmax 함수 topK 연산을 적용하는 것과 같은 후처리 작업에서도 활용할 수 있습니다. 유연성이 뛰어난 BNNSGraphBuilder API는 오디오 효과에도 적용될 수 있습니다. 이 API를 통해 비트크러싱과 같은 실시간 오디오 효과를 만들 수 있습니다. 이때 32비트에 비해 16비트 정밀도를 사용하면 성능이 상당히 개선됩니다. BNNSGraphBuilder는 사용자 친화적인 프로그래밍 인터페이스를 제공하며, 실시간 및 지연 시간에 민감한 사용 사례부터 일반적인 머신 러닝 작업까지 다양한 분야의 애플리케이션에서 접근할 수 있습니다.